Skip to content

Commit d5a3a8c

Browse files
DROID-3866 Chat | Camera permission handling (#2656)
1 parent 0d4c7f0 commit d5a3a8c

File tree

10 files changed

+111
-16
lines changed

10 files changed

+111
-16
lines changed

app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import com.anytypeio.anytype.core_ui.features.multiplayer.GenerateInviteLinkCard
4747
import com.anytypeio.anytype.core_ui.features.multiplayer.ShareInviteLinkCard
4848
import com.anytypeio.anytype.core_ui.views.BaseAlertDialog
4949
import com.anytypeio.anytype.core_utils.ext.arg
50+
import com.anytypeio.anytype.core_utils.ext.openAppSettings
5051
import com.anytypeio.anytype.core_utils.ext.toast
5152
import com.anytypeio.anytype.core_utils.intents.SystemAction.OpenUrl
5253
import com.anytypeio.anytype.core_utils.intents.proceedWithAction
@@ -69,6 +70,7 @@ import com.anytypeio.anytype.ui.profile.ParticipantFragment
6970
import com.anytypeio.anytype.ui.search.GlobalSearchScreen
7071
import com.anytypeio.anytype.ui.sets.ObjectSetFragment
7172
import com.anytypeio.anytype.ui.settings.typography
73+
import com.anytypeio.anytype.ui.vault.AlertScreenModals
7274
import javax.inject.Inject
7375
import timber.log.Timber
7476

@@ -504,6 +506,19 @@ class ChatFragment : BaseComposeFragment() {
504506
onDismissRequest = vm::hideError
505507
)
506508
}
509+
ChatViewModel.UiErrorState.CameraPermissionDenied -> {
510+
AlertScreenModals(
511+
title = getString(R.string.camera_permission_required_title),
512+
description = getString(R.string.camera_permission_settings_message),
513+
firstButtonText = getString(R.string.open_settings),
514+
secondButtonText = getString(R.string.cancel),
515+
onAction = {
516+
requireContext().openAppSettings()
517+
vm.hideError()
518+
},
519+
onDismiss = vm::hideError
520+
)
521+
}
507522
}
508523
}
509524

app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.anytypeio.anytype.ui.vault
22

3+
import android.Manifest
4+
import android.content.pm.PackageManager
35
import android.os.Build.VERSION.SDK_INT
46
import android.os.Bundle
57
import android.view.LayoutInflater
@@ -9,10 +11,10 @@ import androidx.activity.result.contract.ActivityResultContracts
911
import androidx.compose.material.MaterialTheme
1012
import androidx.compose.material3.ExperimentalMaterial3Api
1113
import androidx.compose.runtime.LaunchedEffect
12-
import com.google.zxing.integration.android.IntentIntegrator
1314
import androidx.compose.ui.platform.ComposeView
1415
import androidx.compose.ui.platform.ViewCompositionStrategy
1516
import androidx.compose.ui.res.stringResource
17+
import androidx.core.content.ContextCompat
1618
import androidx.core.os.bundleOf
1719
import androidx.fragment.app.viewModels
1820
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -22,6 +24,7 @@ import com.anytypeio.anytype.R
2224
import com.anytypeio.anytype.core_models.chats.NotificationState
2325
import com.anytypeio.anytype.core_ui.views.BaseAlertDialog
2426
import com.anytypeio.anytype.core_utils.ext.argOrNull
27+
import com.anytypeio.anytype.core_utils.ext.openAppSettings
2528
import com.anytypeio.anytype.core_utils.ext.toast
2629
import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK
2730
import com.anytypeio.anytype.core_utils.intents.ActivityCustomTabsHelper
@@ -47,6 +50,7 @@ import com.anytypeio.anytype.ui.spaces.CreateSpaceFragment.Companion.ARG_SPACE_T
4750
import com.anytypeio.anytype.ui.spaces.CreateSpaceFragment.Companion.TYPE_CHAT
4851
import com.anytypeio.anytype.ui.spaces.CreateSpaceFragment.Companion.TYPE_SPACE
4952
import com.anytypeio.anytype.ui.spaces.DeleteSpaceWarning
53+
import com.google.zxing.integration.android.IntentIntegrator
5054
import javax.inject.Inject
5155
import timber.log.Timber
5256

@@ -74,6 +78,16 @@ class VaultFragment : BaseComposeFragment() {
7478
vm.onQrScannerError()
7579
}
7680
}
81+
82+
private val cameraPermissionLauncher = registerForActivityResult(
83+
ActivityResultContracts.RequestPermission()
84+
) { isGranted: Boolean ->
85+
if (isGranted) {
86+
launchQrScanner()
87+
} else {
88+
vm.onShowCameraPermissionSettingsDialog()
89+
}
90+
}
7791

7892
@OptIn(ExperimentalMaterial3Api::class)
7993
override fun onCreateView(
@@ -135,7 +149,7 @@ class VaultFragment : BaseComposeFragment() {
135149
}
136150

137151
VaultErrors.QrCodeIsNotValid -> {
138-
VaultAlertScreenModals(
152+
AlertScreenModals(
139153
title = getString(R.string.vault_qr_invalid_title),
140154
description = getString(R.string.vault_qr_invalid_description),
141155
firstButtonText = getString(R.string.vault_qr_try_again),
@@ -145,14 +159,28 @@ class VaultFragment : BaseComposeFragment() {
145159
}
146160

147161
VaultErrors.QrScannerError -> {
148-
VaultAlertScreenModals(
162+
AlertScreenModals(
149163
title = getString(R.string.vault_qr_scan_error_title),
150164
description = getString(R.string.vault_qr_scan_error_description),
151165
firstButtonText = getString(R.string.vault_qr_try_again),
152166
onAction = vm::onModalTryAgainClicked,
153167
onDismiss = vm::onModalCancelClicked
154168
)
155169
}
170+
171+
VaultErrors.CameraPermissionDenied -> {
172+
AlertScreenModals(
173+
title = getString(R.string.camera_permission_required_title),
174+
description = getString(R.string.camera_permission_settings_message),
175+
firstButtonText = getString(R.string.open_settings),
176+
secondButtonText = getString(R.string.cancel),
177+
onAction = {
178+
requireContext().openAppSettings()
179+
vm.clearVaultError()
180+
},
181+
onDismiss = vm::clearVaultError
182+
)
183+
}
156184
}
157185

158186
if (vm.showChooseSpaceType.collectAsStateWithLifecycle().value) {
@@ -317,12 +345,7 @@ class VaultFragment : BaseComposeFragment() {
317345
}
318346

319347
VaultCommand.ScanQrCode -> {
320-
qrCodeLauncher.launch(
321-
IntentIntegrator
322-
.forSupportFragment(this)
323-
.setBeepEnabled(false)
324-
.createScanIntent()
325-
)
348+
handleCameraPermissionAndScan()
326349
}
327350

328351
is VaultCommand.NavigateToRequestJoinSpace -> {
@@ -478,6 +501,29 @@ class VaultFragment : BaseComposeFragment() {
478501
componentManager().vaultComponent.release()
479502
}
480503

504+
private fun handleCameraPermissionAndScan() {
505+
when {
506+
ContextCompat.checkSelfPermission(
507+
requireContext(),
508+
Manifest.permission.CAMERA
509+
) == PackageManager.PERMISSION_GRANTED -> {
510+
launchQrScanner()
511+
}
512+
else -> {
513+
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
514+
}
515+
}
516+
}
517+
518+
private fun launchQrScanner() {
519+
qrCodeLauncher.launch(
520+
IntentIntegrator
521+
.forSupportFragment(this)
522+
.setBeepEnabled(false)
523+
.createScanIntent()
524+
)
525+
}
526+
481527
companion object {
482528
private const val SHOW_MNEMONIC_KEY = "arg.vault-screen.show-mnemonic"
483529
private const val DEEP_LINK_KEY = "arg.vault-screen.deep-link"

app/src/main/java/com/anytypeio/anytype/ui/vault/VaultScreenAlertModals.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import com.anytypeio.anytype.core_ui.foundation.Prompt
1616

1717
@OptIn(ExperimentalMaterial3Api::class)
1818
@Composable
19-
fun VaultAlertScreenModals(
19+
fun AlertScreenModals(
2020
title: String,
2121
description: String,
2222
firstButtonText: String,

core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/AndroidExtension.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,4 +467,20 @@ fun Context.openNotificationSettings() {
467467
}
468468
}
469469
startActivity(intent)
470+
}
471+
472+
/**
473+
* Opens the application's settings screen in the system settings.
474+
*
475+
* This extension function launches an intent that navigates the user to the
476+
* app-specific settings page, where permissions and other options can be managed.
477+
* Typically used when prompting the user to manually adjust app permissions.
478+
*
479+
* @receiver Context used to start the settings activity.
480+
*/
481+
fun Context.openAppSettings() {
482+
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
483+
data = Uri.fromParts("package", packageName, null)
484+
}
485+
startActivity(intent)
470486
}

feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,6 +1606,10 @@ class ChatViewModel @Inject constructor(
16061606
fun hideError() {
16071607
errorState.value = UiErrorState.Hidden
16081608
}
1609+
1610+
fun onCameraPermissionDenied() {
1611+
errorState.value = UiErrorState.CameraPermissionDenied
1612+
}
16091613

16101614
private fun proceedWithSpaceSubscription() {
16111615
viewModelScope.launch {
@@ -1711,6 +1715,7 @@ class ChatViewModel @Inject constructor(
17111715
sealed class UiErrorState {
17121716
data object Hidden : UiErrorState()
17131717
data class Show(val msg: String) : UiErrorState()
1718+
data object CameraPermissionDenied : UiErrorState()
17141719
}
17151720

17161721
sealed class Params {

feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
8282
import com.anytypeio.anytype.core_ui.views.Caption1Medium
8383
import com.anytypeio.anytype.core_ui.views.Caption1Regular
8484
import com.anytypeio.anytype.core_ui.views.ContentMiscChat
85-
import com.anytypeio.anytype.core_utils.ext.toast
8685
import com.anytypeio.anytype.feature_chats.R
8786
import com.anytypeio.anytype.feature_chats.presentation.ChatView
8887
import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel.ChatBoxMode
@@ -110,6 +109,7 @@ fun ChatBox(
110109
onClearReplyClicked: () -> Unit,
111110
onChatBoxMediaPicked: (List<Uri>) -> Unit,
112111
onChatBoxFilePicked: (List<Uri>) -> Unit,
112+
onCameraPermissionDenied: () -> Unit = {},
113113
onExitEditMessageMode: () -> Unit,
114114
onValueChange: (TextFieldValue, List<ChatBoxSpan>) -> Unit,
115115
onUrlInserted: (Url) -> Unit,
@@ -168,7 +168,7 @@ fun ChatBox(
168168
onUriReceived = { capturedImageUri = it.toString() }
169169
)
170170
} else {
171-
context.toast(context.getString(R.string.chat_camera_permission_denied))
171+
onCameraPermissionDenied()
172172
}
173173
}
174174

@@ -182,7 +182,7 @@ fun ChatBox(
182182
onUriReceived = { capturedVideoUri = it.toString() }
183183
)
184184
} else {
185-
context.toast(context.getString(R.string.chat_camera_permission_denied))
185+
onCameraPermissionDenied()
186186
}
187187
}
188188

feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,8 @@ fun ChatScreenWrapper(
259259
)
260260
},
261261
onRequestVideoPlayer = onRequestVideoPlayer,
262-
onCreateAndAttachObject = vm::onCreateAndAttachObject
262+
onCreateAndAttachObject = vm::onCreateAndAttachObject,
263+
onCameraPermissionDenied = vm::onCameraPermissionDenied
263264
)
264265
LaunchedEffect(Unit) {
265266
vm.uXCommands.collect { command ->
@@ -395,7 +396,8 @@ fun ChatScreen(
395396
canCreateInviteLink: Boolean = false,
396397
isReadOnly: Boolean = false,
397398
onRequestVideoPlayer: (ChatView.Message.Attachment.Video) -> Unit = {},
398-
onCreateAndAttachObject: () -> Unit
399+
onCreateAndAttachObject: () -> Unit,
400+
onCameraPermissionDenied: () -> Unit = {}
399401
) {
400402

401403
Timber.d("DROID-2966 Render called with state, number of messages: ${messages.size}")
@@ -888,7 +890,8 @@ fun ChatScreen(
888890
onUrlInserted = onUrlInserted,
889891
onImageCaptured = onImageCaptured,
890892
onVideoCaptured = onVideoCaptured,
891-
onCreateAndAttachObject = onCreateAndAttachObject
893+
onCreateAndAttachObject = onCreateAndAttachObject,
894+
onCameraPermissionDenied = onCameraPermissionDenied
892895
)
893896
}
894897
}

localization/src/main/res/values/strings.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2159,5 +2159,10 @@ Please provide specific details of your needs here.</string>
21592159
<string name="vault_qr_invalid_title">Invalid QR code</string>
21602160
<string name="vault_qr_invalid_description">The scanned QR code doesn\'t match any valid invitation. Please check it and try again.</string>
21612161
<string name="vault_qr_try_again">Try again</string>
2162+
<string name="camera_permission_required_title">Camera permission required</string>
2163+
<string name="camera_permission_rationale_message">To scan QR codes, Anytype needs access to your camera. This permission is only used for scanning QR codes.</string>
2164+
<string name="camera_permission_settings_message">To scan QR codes, you need to allow camera access in your device settings.</string>
2165+
<string name="open_settings">Open Settings</string>
2166+
<string name="allow">Allow</string>
21622167

21632168
</resources>

presentation/src/main/java/com/anytypeio/anytype/presentation/vault/Models.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,5 @@ sealed class VaultErrors {
110110
data object MaxPinnedSpacesReached : VaultErrors()
111111
data object QrScannerError : VaultErrors()
112112
data object QrCodeIsNotValid : VaultErrors()
113+
data object CameraPermissionDenied : VaultErrors()
113114
}

presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,10 @@ class VaultViewModel(
815815
fun clearVaultError() {
816816
vaultErrors.value = VaultErrors.Hidden
817817
}
818+
819+
fun onShowCameraPermissionSettingsDialog() {
820+
vaultErrors.value = VaultErrors.CameraPermissionDenied
821+
}
818822

819823
fun onPinSpaceClicked(spaceId: Id) {
820824
val state = uiState.value

0 commit comments

Comments
 (0)