From a99db2b16e59a94d68390d86cdf309f906f42238 Mon Sep 17 00:00:00 2001 From: mtang-signal Date: Fri, 17 May 2024 10:48:40 -0700 Subject: [PATCH] Update camera permission UI for usernames. --- .../main/UsernameLinkSettingsFragment.kt | 27 ++++++++++++---- .../main/UsernameQrScanScreen.kt | 32 ++++++++++++++++++- .../main/UsernameQrScannerActivity.kt | 27 ++++++++++++++++ .../recipients/ui/findby/FindByActivity.kt | 10 +----- 4 files changed, 79 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsFragment.kt index 47216c2353..f499396d1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsFragment.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main +import android.Manifest import android.app.Activity import android.content.Intent import android.content.res.Configuration @@ -77,6 +78,7 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.Username import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsState.ActiveTab import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.permissions.PermissionCompat +import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.util.CommunicationActions import java.io.ByteArrayOutputStream @@ -140,6 +142,7 @@ class UsernameLinkSettingsFragment : ComposeFragment() { onShareBadge = { shareQrBadge(requireActivity(), viewModel.generateQrCodeImage(helpText)) }, onQrCodeScanned = { data -> viewModel.onQrCodeScanned(data) }, onQrResultHandled = { viewModel.onQrResultHandled() }, + onOpenCameraClicked = { askCameraPermissions() }, onOpenGalleryClicked = { if (galleryPermissionState.allPermissionsGranted) { galleryLauncher.launch(Unit) @@ -153,6 +156,10 @@ class UsernameLinkSettingsFragment : ComposeFragment() { ) } + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { disposables.bindTo(viewLifecycleOwner) } @@ -161,6 +168,15 @@ class UsernameLinkSettingsFragment : ComposeFragment() { super.onResume() viewModel.onResume() } + + private fun askCameraPermissions() { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, parentFragmentManager) + .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() } + .execute() + } } @OptIn(ExperimentalPermissionsApi::class) @@ -177,6 +193,7 @@ private fun MainScreen( onShareBadge: () -> Unit = {}, onQrCodeScanned: (String) -> Unit = {}, onQrResultHandled: () -> Unit = {}, + onOpenCameraClicked: () -> Unit = {}, onOpenGalleryClicked: () -> Unit = {}, onLinkReset: () -> Unit = {}, onBackNavigationPressed: () -> Unit = {}, @@ -244,7 +261,9 @@ private fun MainScreen( qrScanResult = state.qrScanResult, onQrCodeScanned = onQrCodeScanned, onQrResultHandled = onQrResultHandled, + onOpenCameraClicked = onOpenCameraClicked, onOpenGalleryClicked = onOpenGalleryClicked, + hasCameraPermission = cameraPermissionState.status.isGranted, modifier = Modifier.padding(contentPadding), onRecipientFound = { recipient -> val taskStack = TaskStackBuilder @@ -296,13 +315,7 @@ private fun TopAppBarContent( TabButton( label = stringResource(R.string.UsernameLinkSettings_scan_tab_name), active = activeTab == ActiveTab.Scan, - onClick = { - if (cameraPermissionState.status.isGranted) { - onScanTabSelected() - } else { - cameraPermissionState.launchPermissionRequest() - } - }, + onClick = { onScanTabSelected() }, modifier = Modifier.padding(end = 8.dp) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanScreen.kt index 16d2e4b1ce..8861a9e62c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -26,11 +27,13 @@ import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.LifecycleOwner import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign +import org.signal.core.ui.Buttons import org.signal.core.ui.Dialogs import org.signal.core.ui.theme.SignalTheme import org.signal.qr.QrScannerView @@ -49,8 +52,10 @@ fun UsernameQrScanScreen( qrScanResult: QrScanResult?, onQrCodeScanned: (String) -> Unit, onQrResultHandled: () -> Unit, + onOpenCameraClicked: () -> Unit, onOpenGalleryClicked: () -> Unit, onRecipientFound: (Recipient) -> Unit, + hasCameraPermission: Boolean, modifier: Modifier = Modifier ) { val path = remember { Path() } @@ -113,9 +118,34 @@ fun UsernameQrScanScreen( .fillMaxHeight() .drawWithContent { drawContent() - drawQrCrosshair(path) + if (hasCameraPermission) { + drawQrCrosshair(path) + } } ) + if (!hasCameraPermission) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .align(Alignment.Center) + .padding(50.dp) + ) { + Text( + text = stringResource(R.string.CameraXFragment_to_scan_qr_code_allow_camera), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge, + color = Color.White, + modifier = Modifier.padding(10.dp) + ) + Buttons.MediumTonal( + colors = ButtonDefaults.filledTonalButtonColors(), + onClick = onOpenCameraClicked + ) { + Text(stringResource(R.string.CameraXFragment_allow_access)) + } + } + } FloatingActionButton( shape = CircleShape, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScannerActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScannerActivity.kt index ebcc11a95e..ba17bd07d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScannerActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScannerActivity.kt @@ -7,6 +7,8 @@ package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main +import android.Manifest +import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Bundle @@ -30,7 +32,10 @@ import androidx.compose.ui.res.stringResource import androidx.lifecycle.LifecycleOwner import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.MultiplePermissionsState +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberMultiplePermissionsState +import com.google.accompanist.permissions.rememberPermissionState import io.reactivex.rxjava3.disposables.CompositeDisposable import org.signal.core.ui.Dialogs import org.signal.core.ui.theme.SignalTheme @@ -38,6 +43,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.getParcelableExtraCompat import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.permissions.PermissionCompat +import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.DynamicTheme @@ -55,6 +61,11 @@ class UsernameQrScannerActivity : AppCompatActivity() { private val viewModel: UsernameQrScannerViewModel by viewModels() private val disposables = LifecycleDisposable() + @SuppressLint("MissingSuperCall") + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) disposables.bindTo(this) @@ -74,6 +85,7 @@ class UsernameQrScannerActivity : AppCompatActivity() { } } + val cameraPermissionState: PermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) val state by viewModel.state SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current)) { @@ -82,10 +94,12 @@ class UsernameQrScannerActivity : AppCompatActivity() { diposables = disposables.disposables, state = state, galleryPermissionsState = galleryPermissionState, + cameraPermissionState = cameraPermissionState, onQrScanned = { url -> viewModel.onQrScanned(url) }, onQrResultHandled = { finish() }, + onOpenCameraClicked = { askCameraPermissions() }, onOpenGalleryClicked = { if (galleryPermissionState.allPermissionsGranted) { galleryLauncher.launch(Unit) @@ -108,6 +122,15 @@ class UsernameQrScannerActivity : AppCompatActivity() { } } + private fun askCameraPermissions() { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, supportFragmentManager) + .onAnyDenied { Toast.makeText(this, R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() } + .execute() + } + class Contract : ActivityResultContract() { override fun createIntent(context: Context, input: Unit): Intent { return Intent(context, UsernameQrScannerActivity::class.java) @@ -126,8 +149,10 @@ fun Content( diposables: CompositeDisposable, state: UsernameQrScannerViewModel.ScannerState, galleryPermissionsState: MultiplePermissionsState, + cameraPermissionState: PermissionState, onQrScanned: (String) -> Unit, onQrResultHandled: () -> Unit, + onOpenCameraClicked: () -> Unit, onOpenGalleryClicked: () -> Unit, onRecipientFound: (Recipient) -> Unit, onBackNavigationPressed: () -> Unit @@ -155,8 +180,10 @@ fun Content( qrScanResult = state.qrScanResult, onQrCodeScanned = onQrScanned, onQrResultHandled = onQrResultHandled, + onOpenCameraClicked = onOpenCameraClicked, onOpenGalleryClicked = onOpenGalleryClicked, onRecipientFound = onRecipientFound, + hasCameraPermission = cameraPermissionState.status.isGranted, modifier = Modifier.padding(contentPadding) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByActivity.kt index 9e8f75016c..9ae4ba9985 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByActivity.kt @@ -81,7 +81,6 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameQrScannerActivity import org.thoughtcrime.securesms.invites.InviteActions -import org.thoughtcrime.securesms.permissions.compose.Permissions import org.thoughtcrime.securesms.phonenumbers.PhoneNumberVisualTransformation import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.registration.util.CountryPrefix @@ -142,13 +141,6 @@ class FindByActivity : PassphraseRequiredActivity() { ) { val context = LocalContext.current - val cameraPermissionController = Permissions.cameraPermissionHandler( - rationale = stringResource(id = R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs_access_to_the_camera), - onPermissionGranted = { - qrScanLauncher.launch(Unit) - } - ) - Content( paddingValues = it, state = state, @@ -171,7 +163,7 @@ class FindByActivity : PassphraseRequiredActivity() { navController.navigate("select-country-prefix") }, onQrCodeScanClicked = { - cameraPermissionController.request() + qrScanLauncher.launch(Unit) } ) }