11package cz.muni.fi.rpg.ui.joinParty
22
33import android.Manifest
4- import android.os.Parcelable
5- import android.widget.Toast
4+ import android.content.Context
5+ import android.content.Intent
6+ import android.net.Uri
7+ import android.provider.Settings
68import androidx.compose.foundation.layout.Arrangement
79import androidx.compose.foundation.layout.Column
10+ import androidx.compose.foundation.layout.ColumnScope
811import androidx.compose.foundation.layout.fillMaxHeight
912import androidx.compose.foundation.layout.fillMaxSize
13+ import androidx.compose.foundation.layout.fillMaxWidth
14+ import androidx.compose.foundation.layout.padding
15+ import androidx.compose.foundation.rememberScrollState
16+ import androidx.compose.foundation.verticalScroll
17+ import androidx.compose.material.MaterialTheme
1018import androidx.compose.material.Scaffold
1119import androidx.compose.material.Text
20+ import androidx.compose.material.TextButton
1221import androidx.compose.material.TopAppBar
1322import androidx.compose.runtime.Composable
14- import androidx.compose.runtime.LaunchedEffect
23+ import androidx.compose.runtime.SideEffect
1524import androidx.compose.runtime.getValue
1625import androidx.compose.runtime.mutableStateOf
1726import androidx.compose.runtime.rememberCoroutineScope
1827import androidx.compose.runtime.saveable.rememberSaveable
1928import androidx.compose.runtime.setValue
2029import androidx.compose.ui.Alignment
2130import androidx.compose.ui.Modifier
31+ import androidx.compose.ui.platform.LocalContext
2232import androidx.compose.ui.res.stringResource
23- import com.eazypermissions.common.model.PermissionResult
24- import com.eazypermissions.coroutinespermission.PermissionManager
33+ import androidx.compose.ui.text.style.TextAlign
34+ import com.google.accompanist.permissions.PermissionState
35+ import com.google.accompanist.permissions.rememberPermissionState
2536import cz.frantisekmasa.wfrp_master.core.domain.party.Invitation
2637import cz.frantisekmasa.wfrp_master.core.ui.buttons.BackButton
38+ import cz.frantisekmasa.wfrp_master.core.ui.primitives.HorizontalLine
39+ import cz.frantisekmasa.wfrp_master.core.ui.primitives.Spacing
2740import cz.frantisekmasa.wfrp_master.core.ui.scaffolding.SubheadBar
28- import cz.frantisekmasa.wfrp_master.core.ui.viewinterop.LocalActivity
2941import cz.frantisekmasa.wfrp_master.navigation.Route
3042import cz.frantisekmasa.wfrp_master.navigation.Routing
3143import cz.muni.fi.rpg.R
32- import cz.muni.fi.rpg.ui.common.toast
44+ import cz.muni.fi.rpg.viewModels.JoinPartyViewModel
3345import cz.muni.fi.rpg.viewModels.provideJoinPartyViewModel
3446import kotlinx.coroutines.launch
35- import kotlinx.parcelize.Parcelize
3647
3748@Composable
3849fun InvitationScannerScreen (routing : Routing <Route >) {
3950 Scaffold (
4051 topBar = {
4152 TopAppBar (
42- title = { Text (stringResource(R .string.qr_scan_prompt )) },
53+ title = { Text (stringResource(R .string.title_joinParty )) },
4354 navigationIcon = {
4455 BackButton (onClick = { routing.pop() })
4556 },
@@ -54,81 +65,124 @@ fun InvitationScannerScreen(routing: Routing<Route>) {
5465 modifier = Modifier .fillMaxHeight()
5566 ) {
5667 val viewModel = provideJoinPartyViewModel()
57- val coroutineScope = rememberCoroutineScope()
5868
59- var screenState: InvitationScannerScreenState by rememberSaveable {
60- mutableStateOf(InvitationScannerScreenState .WaitingForPermissions )
61- }
69+ var invitation: Invitation ? by rememberSaveable { mutableStateOf(null ) }
6270
63- when (val state = screenState) {
64- InvitationScannerScreenState .WaitingForPermissions -> {
65- val activity = LocalActivity .current
66-
67- LaunchedEffect (null ) {
68- val permissionResult = PermissionManager .requestPermissions(
69- activity,
70- PermissionRequestCode ,
71- Manifest .permission.CAMERA ,
72- )
73-
74- when (permissionResult) {
75- is PermissionResult .PermissionGranted -> {
76- screenState = InvitationScannerScreenState .Scanning
77- }
78- else -> {
79- // TODO: Add more specific wording for types of denial
80- // see https://github.com/sagar-viradiya/eazypermissions#coroutines-support
81-
82- activity.toast(
83- activity.getString(R .string.error_camera_permission_required),
84- Toast .LENGTH_LONG
85- )
86-
87- routing.pop()
88- }
89- }
90- }
91- }
92- InvitationScannerScreenState .Scanning -> {
93- SubheadBar (stringResource(R .string.qr_scan_prompt))
94- QrCodeScanner (
95- modifier = Modifier .fillMaxSize(),
96- onSuccessfulScan = { qrCodeData ->
97- coroutineScope.launch {
98- viewModel.deserializeInvitationJson(qrCodeData)?.let {
99- screenState =
100- InvitationScannerScreenState .WaitingForUserConfirmation (it)
101- }
102- }
103- },
104- )
105- }
106- is InvitationScannerScreenState .WaitingForUserConfirmation -> {
71+ when {
72+ invitation != null -> {
10773 InvitationConfirmation (
108- state. invitation,
74+ invitation!! ,
10975 viewModel,
110- onSuccess = {
111- routing.pop()
112- },
113- onError = {
114- screenState = InvitationScannerScreenState .Scanning
115- },
76+ onSuccess = { routing.pop() },
77+ onError = { invitation = null },
11678 )
11779 }
80+ else -> {
81+ Scanner (viewModel, onSuccessfulScan = { invitation = it })
82+ }
11883 }
11984 }
12085 }
12186}
12287
123- private sealed class InvitationScannerScreenState : Parcelable {
124- @Parcelize
125- object WaitingForPermissions : InvitationScannerScreenState()
88+ @Composable
89+ private fun Scanner (viewModel : JoinPartyViewModel , onSuccessfulScan : (Invitation ) -> Unit ) {
90+ val coroutineScope = rememberCoroutineScope()
91+ val camera = rememberPermissionState(Manifest .permission.CAMERA )
12692
127- @Parcelize
128- object Scanning : InvitationScannerScreenState()
93+ when {
94+ camera.hasPermission -> {
95+ SubheadBar (stringResource(R .string.qr_scan_prompt))
96+ QrCodeScanner (
97+ modifier = Modifier .fillMaxSize(),
98+ onSuccessfulScan = { qrCodeData ->
99+ coroutineScope.launch {
100+ viewModel.deserializeInvitationJson(qrCodeData)
101+ ?.let (onSuccessfulScan)
102+ }
103+ },
104+ )
105+ }
106+ ! camera.permissionRequested || camera.shouldShowRationale -> PermissionRequestScreen (camera)
107+ else -> PermissionDeniedScreen ()
108+ }
109+ }
110+
111+ @Composable
112+ private fun PermissionRequestScreen (camera : PermissionState ) {
113+ ScreenBody {
114+ if (! camera.permissionRequested) {
115+ SideEffect { camera.launchPermissionRequest() }
116+ }
129117
130- @Parcelize
131- data class WaitingForUserConfirmation (val invitation : Invitation ) : InvitationScannerScreenState()
118+ Text (
119+ stringResource(R .string.camera_permission_required),
120+ style = MaterialTheme .typography.h6,
121+ )
122+
123+ Rationale ()
124+
125+ TextButton (onClick = { camera.launchPermissionRequest() }) {
126+ Text (stringResource(R .string.button_request_permission).uppercase())
127+ }
128+
129+ Alternative ()
130+ }
132131}
133132
134- private const val PermissionRequestCode = 10
133+ @Composable
134+ private fun PermissionDeniedScreen () {
135+ ScreenBody {
136+ Text (stringResource(R .string.camera_permission_denied), style = MaterialTheme .typography.h6)
137+ Rationale ()
138+ Text (stringResource(R .string.camera_permission_instructions), textAlign = TextAlign .Center )
139+
140+ val context = LocalContext .current
141+ TextButton (onClick = { context.openApplicationSettings() }) {
142+ Text (stringResource(R .string.button_open_settings).uppercase())
143+ }
144+
145+ Alternative ()
146+ }
147+ }
148+
149+ @Composable
150+ private inline fun ScreenBody (content : @Composable ColumnScope .() -> Unit ) {
151+ Column (
152+ Modifier
153+ .fillMaxWidth()
154+ .padding(Spacing .bodyPadding)
155+ .verticalScroll(rememberScrollState()),
156+ horizontalAlignment = Alignment .CenterHorizontally ,
157+ content = content,
158+ )
159+ }
160+
161+ @Composable
162+ private fun Rationale () {
163+ Text (
164+ stringResource(R .string.camera_permission_rationale),
165+ textAlign = TextAlign .Center ,
166+ )
167+ }
168+
169+ @Composable
170+ private fun Alternative () {
171+
172+ HorizontalLine ()
173+
174+ Text (
175+ stringResource(R .string.camera_permission_alternative),
176+ modifier = Modifier .padding(top = Spacing .mediumLarge),
177+ textAlign = TextAlign .Center ,
178+ )
179+ }
180+
181+ private fun Context.openApplicationSettings () {
182+ startActivity(
183+ Intent ().apply {
184+ action = Settings .ACTION_APPLICATION_DETAILS_SETTINGS
185+ data = Uri .fromParts(" package" , packageName, null )
186+ }
187+ )
188+ }
0 commit comments