diff --git a/app/src/main/java/com/getcode/Session.kt b/app/src/main/java/com/getcode/Session.kt index e974adb0..973a89ed 100644 --- a/app/src/main/java/com/getcode/Session.kt +++ b/app/src/main/java/com/getcode/Session.kt @@ -81,6 +81,7 @@ import com.getcode.network.repository.hexEncodedString import com.getcode.network.repository.toPublicKey import com.getcode.solana.organizer.GiftCardAccount import com.getcode.solana.organizer.Organizer +import com.getcode.ui.components.PermissionResult import com.getcode.util.CurrencyUtils import com.getcode.util.IntentUtils import com.getcode.util.Kin @@ -465,8 +466,8 @@ class Session @Inject constructor( uiFlow.update { it.copy(isCameraScanEnabled = scanning) } } - fun onCameraPermissionChanged(isGranted: Boolean) { - uiFlow.update { it.copy(isCameraPermissionGranted = isGranted) } + fun onCameraPermissionResult(result: PermissionResult) { + uiFlow.update { it.copy(isCameraPermissionGranted = result == PermissionResult.Granted) } } fun showBill( diff --git a/app/src/main/java/com/getcode/ui/components/PermissionCheck.kt b/app/src/main/java/com/getcode/ui/components/PermissionCheck.kt index 4ee63b2e..e5e5fa84 100644 --- a/app/src/main/java/com/getcode/ui/components/PermissionCheck.kt +++ b/app/src/main/java/com/getcode/ui/components/PermissionCheck.kt @@ -1,39 +1,85 @@ package com.getcode.ui.components +import android.app.Activity import android.content.Context import android.content.pm.PackageManager import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import com.getcode.ui.utils.getActivity + +enum class PermissionResult { + Granted, Denied, ShouldShowRationale +} typealias PermissionsLauncher = ManagedActivityResultLauncher + @Composable -fun getPermissionLauncher(onPermissionResult: (isGranted: Boolean) -> Unit) = - rememberLauncherForActivityResult( +fun getPermissionLauncher( + permission: String, + onPermissionResult: (result: PermissionResult) -> Unit +): PermissionsLauncher { + val context = LocalContext.current + val activity = context as Activity + + val launcher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> - onPermissionResult(isGranted) + // This block will be triggered after the user chooses to grant or deny the permission + // and we can tell if the user permanently declines or if we need to show rational + val permissionPermanentlyDenied = !ActivityCompat.shouldShowRequestPermissionRationale( + activity, permission + ) && !isGranted + + when { + permissionPermanentlyDenied -> { + onPermissionResult(PermissionResult.ShouldShowRationale) + } + !isGranted -> onPermissionResult(PermissionResult.Denied) + else -> onPermissionResult(PermissionResult.Granted) + } } -object PermissionCheck { - fun requestPermission( - context: Context, + return launcher +} + +@Composable +fun rememberPermissionChecker(): PermissionChecker { + val context = LocalContext.current + return remember(context) { + PermissionChecker(context) + } +} + +class PermissionChecker(private val context: Context) { + fun request( permission: String, - shouldRequest: Boolean, - onPermissionResult: (isGranted: Boolean) -> Unit, - launcher: PermissionsLauncher + shouldRequest: Boolean = true, + launcher: PermissionsLauncher, + onPermissionResult: (result: PermissionResult) -> Unit = { }, ) { + val activity = context.getActivity() + when (ContextCompat.checkSelfPermission(context, permission)) { PackageManager.PERMISSION_GRANTED -> { - onPermissionResult(true) + onPermissionResult(PermissionResult.Granted) } PackageManager.PERMISSION_DENIED -> { if (shouldRequest) { launcher.launch(permission) } else { - onPermissionResult(false) + if (activity != null) { + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) { + onPermissionResult(PermissionResult.ShouldShowRationale) + return + } + } + onPermissionResult(PermissionResult.Denied) } } } diff --git a/app/src/main/java/com/getcode/view/login/AccessKey.kt b/app/src/main/java/com/getcode/view/login/AccessKey.kt index 95ab0fab..203f70ec 100644 --- a/app/src/main/java/com/getcode/view/login/AccessKey.kt +++ b/app/src/main/java/com/getcode/view/login/AccessKey.kt @@ -52,15 +52,16 @@ import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.navigation.screens.LoginArgs import com.getcode.theme.CodeTheme import com.getcode.theme.White -import com.getcode.ui.utils.measured -import com.getcode.ui.components.SelectionContainer import com.getcode.ui.components.ButtonState import com.getcode.ui.components.Cloudy import com.getcode.ui.components.CodeButton -import com.getcode.ui.components.PermissionCheck +import com.getcode.ui.components.PermissionResult +import com.getcode.ui.components.SelectionContainer import com.getcode.ui.components.getPermissionLauncher +import com.getcode.ui.components.rememberPermissionChecker import com.getcode.ui.components.rememberSelectionState import com.getcode.ui.utils.addIf +import com.getcode.ui.utils.measured import com.getcode.util.launchAppSettings @OptIn(ExperimentalComposeUiApi::class) @@ -80,8 +81,8 @@ fun AccessKey( var isStoragePermissionGranted by remember { mutableStateOf(false) } val isAccessKeyVisible = remember { MutableTransitionState(false) } - val onPermissionResult = { isSuccess: Boolean -> - isStoragePermissionGranted = isSuccess + val onPermissionResult = { result: PermissionResult -> + isStoragePermissionGranted = result == PermissionResult.Granted if (!isStoragePermissionGranted) { TopBarManager.showMessage( @@ -96,7 +97,8 @@ fun AccessKey( } } - val launcher = getPermissionLauncher(onPermissionResult) + val launcher = getPermissionLauncher(Manifest.permission.WRITE_EXTERNAL_STORAGE, onPermissionResult) + val permissionChecker = rememberPermissionChecker() if (isExportSeedRequested && isStoragePermissionGranted) { viewModel.onSubmit(navigator, true) @@ -109,10 +111,8 @@ fun AccessKey( if (Build.VERSION.SDK_INT > 29) { isStoragePermissionGranted = true } else { - PermissionCheck.requestPermission( - context = context, + permissionChecker.request( permission = Manifest.permission.WRITE_EXTERNAL_STORAGE, - shouldRequest = true, onPermissionResult = onPermissionResult, launcher = launcher ) diff --git a/app/src/main/java/com/getcode/view/login/CameraPermissionCheck.kt b/app/src/main/java/com/getcode/view/login/CameraPermissionCheck.kt index d5c45041..7fc1300c 100644 --- a/app/src/main/java/com/getcode/view/login/CameraPermissionCheck.kt +++ b/app/src/main/java/com/getcode/view/login/CameraPermissionCheck.kt @@ -1,17 +1,22 @@ package com.getcode.view.login import android.Manifest -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext -import com.getcode.App import com.getcode.R import com.getcode.manager.TopBarManager -import com.getcode.ui.components.PermissionCheck +import com.getcode.ui.components.PermissionResult import com.getcode.ui.components.getPermissionLauncher +import com.getcode.ui.components.rememberPermissionChecker import com.getcode.util.launchAppSettings @Composable fun cameraPermissionCheck(isShowError: Boolean = true, onResult: (Boolean) -> Unit): (Boolean) -> Unit { + val permissionChecker = rememberPermissionChecker() val context = LocalContext.current var permissionRequested by remember { mutableStateOf(false) } val onPermissionError = { @@ -25,18 +30,18 @@ fun cameraPermissionCheck(isShowError: Boolean = true, onResult: (Boolean) -> Un ) ) } - val onPermissionResult = { isGranted: Boolean -> + val onPermissionResult = { result: PermissionResult -> + val isGranted = result == PermissionResult.Granted onResult(isGranted) if (!isGranted && permissionRequested && isShowError) { onPermissionError() } Unit } - val launcher = getPermissionLauncher(onPermissionResult) + val launcher = getPermissionLauncher(Manifest.permission.CAMERA, onPermissionResult) val permissionCheck = { shouldRequest: Boolean -> permissionRequested = shouldRequest - PermissionCheck.requestPermission( - context = context, + permissionChecker.request( permission = Manifest.permission.CAMERA, shouldRequest = shouldRequest, onPermissionResult = onPermissionResult, diff --git a/app/src/main/java/com/getcode/view/login/NotificationPermissionCheck.kt b/app/src/main/java/com/getcode/view/login/NotificationPermissionCheck.kt index 04a57acd..389d87ae 100644 --- a/app/src/main/java/com/getcode/view/login/NotificationPermissionCheck.kt +++ b/app/src/main/java/com/getcode/view/login/NotificationPermissionCheck.kt @@ -2,18 +2,37 @@ package com.getcode.view.login import android.Manifest import android.os.Build -import androidx.compose.runtime.* +import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext -import com.getcode.App import com.getcode.R import com.getcode.manager.TopBarManager -import com.getcode.ui.components.PermissionCheck +import com.getcode.ui.components.PermissionResult import com.getcode.ui.components.getPermissionLauncher +import com.getcode.ui.components.rememberPermissionChecker import com.getcode.util.launchAppSettings @Composable -fun notificationPermissionCheck(isShowError: Boolean = true, onResult: (Boolean) -> Unit): (Boolean) -> Unit { +fun notificationPermissionCheck(isShowError: Boolean = true, onResult: (Boolean) -> Unit): (shouldRequest: Boolean) -> Unit { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + notificationPermissionCheckApi33(isShowError, onResult) + } else { + notificationPermissionCheckApiLegacy(isShowError, onResult) + } +} + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +@Composable +private fun notificationPermissionCheckApi33( + isShowError: Boolean = true, + onResult: (Boolean) -> Unit +): (shouldRequest: Boolean) -> Unit { val context = LocalContext.current + val permissionChecker = rememberPermissionChecker() var permissionRequested by remember { mutableStateOf(false) } val onPermissionError = { TopBarManager.showMessage( @@ -26,29 +45,65 @@ fun notificationPermissionCheck(isShowError: Boolean = true, onResult: (Boolean) ) ) } - val onPermissionResult = { isGranted: Boolean -> + val onPermissionResult = { result: PermissionResult -> + val isGranted = result == PermissionResult.Granted onResult(isGranted) if (!isGranted && permissionRequested && isShowError) { onPermissionError() } Unit } - val launcher = getPermissionLauncher(onPermissionResult) + + val launcher = getPermissionLauncher( + Manifest.permission.POST_NOTIFICATIONS, onPermissionResult + ) + val permissionCheck = { shouldRequest: Boolean -> if (Build.VERSION.SDK_INT < 33) { - onPermissionResult(true) + onPermissionResult(PermissionResult.Granted) } else { permissionRequested = shouldRequest - PermissionCheck.requestPermission( - context = context, + permissionChecker.request( permission = Manifest.permission.POST_NOTIFICATIONS, shouldRequest = shouldRequest, onPermissionResult = onPermissionResult, launcher = launcher ) } + } + + return permissionCheck +} +@Composable +private fun notificationPermissionCheckApiLegacy( + isShowError: Boolean = true, + onResult: (Boolean) -> Unit +): (shouldRequest: Boolean) -> Unit { + val context = LocalContext.current + var permissionRequested by remember { mutableStateOf(false) } + val onPermissionError = { + TopBarManager.showMessage( + TopBarManager.TopBarMessage( + title = context.getString(R.string.action_allowPushNotifications), + message = context.getString(R.string.permissions_description_push), + type = TopBarManager.TopBarMessageType.ERROR, + secondaryText = context.getString(R.string.action_openSettings), + secondaryAction = { context.launchAppSettings() } + ) + ) + } + val onPermissionResult = { result: PermissionResult -> + val isGranted = result == PermissionResult.Granted + onResult(isGranted) + if (!isGranted && permissionRequested && isShowError) { + onPermissionError() + } + Unit + } + val permissionCheck = { _: Boolean -> + onPermissionResult(PermissionResult.Granted) } return permissionCheck diff --git a/app/src/main/java/com/getcode/view/login/SeedInput.kt b/app/src/main/java/com/getcode/view/login/SeedInput.kt index 77c7e7cb..41cf16e4 100644 --- a/app/src/main/java/com/getcode/view/login/SeedInput.kt +++ b/app/src/main/java/com/getcode/view/login/SeedInput.kt @@ -44,8 +44,7 @@ fun SeedInput( val focusManager = LocalFocusManager.current val focusRequester = FocusRequester() - val context = LocalContext.current - val launcher = getPermissionLauncher {} + val notificationPermissionCheck = notificationPermissionCheck(isShowError = false) { } Column( modifier = Modifier @@ -123,13 +122,7 @@ fun SeedInput( } if (dataState.isSuccess) { - PermissionCheck.requestPermission( - context = context, - permission = Manifest.permission.POST_NOTIFICATIONS, - shouldRequest = true, - onPermissionResult = {}, - launcher = launcher - ) + notificationPermissionCheck(true) } CodeButton( diff --git a/app/src/main/java/com/getcode/view/main/account/BackupKey.kt b/app/src/main/java/com/getcode/view/main/account/BackupKey.kt index 82566a8b..e724c9a2 100644 --- a/app/src/main/java/com/getcode/view/main/account/BackupKey.kt +++ b/app/src/main/java/com/getcode/view/main/account/BackupKey.kt @@ -39,15 +39,16 @@ import androidx.compose.ui.unit.isSpecified import com.getcode.R import com.getcode.manager.TopBarManager import com.getcode.theme.CodeTheme -import com.getcode.ui.utils.measured -import com.getcode.ui.components.SelectionContainer import com.getcode.ui.components.ButtonState import com.getcode.ui.components.Cloudy import com.getcode.ui.components.CodeButton -import com.getcode.ui.components.PermissionCheck +import com.getcode.ui.components.PermissionResult +import com.getcode.ui.components.SelectionContainer import com.getcode.ui.components.getPermissionLauncher +import com.getcode.ui.components.rememberPermissionChecker import com.getcode.ui.components.rememberSelectionState import com.getcode.ui.utils.addIf +import com.getcode.ui.utils.measured import com.getcode.util.launchAppSettings @Composable @@ -61,8 +62,8 @@ fun BackupKey( var isStoragePermissionGranted by remember { mutableStateOf(false) } val isAccessKeyVisible = remember { MutableTransitionState(false) } - val onPermissionResult = { isSuccess: Boolean -> - isStoragePermissionGranted = isSuccess + val onPermissionResult = { result: PermissionResult -> + isStoragePermissionGranted = result == PermissionResult.Granted if (!isStoragePermissionGranted) { TopBarManager.showMessage( @@ -77,8 +78,8 @@ fun BackupKey( } } - val launcher = getPermissionLauncher(onPermissionResult) - + val launcher = getPermissionLauncher(Manifest.permission.WRITE_EXTERNAL_STORAGE, onPermissionResult) + val permissionChecker = rememberPermissionChecker() if (isExportSeedRequested && isStoragePermissionGranted) { viewModel.onSubmit() isExportSeedRequested = false @@ -90,10 +91,8 @@ fun BackupKey( if (Build.VERSION.SDK_INT > 29) { isStoragePermissionGranted = true } else { - PermissionCheck.requestPermission( - context = context, + permissionChecker.request( permission = Manifest.permission.WRITE_EXTERNAL_STORAGE, - shouldRequest = true, onPermissionResult = onPermissionResult, launcher = launcher ) diff --git a/app/src/main/java/com/getcode/view/main/scanner/ScannerScreen.kt b/app/src/main/java/com/getcode/view/main/scanner/ScannerScreen.kt index 3174e7df..6c4bd3f1 100644 --- a/app/src/main/java/com/getcode/view/main/scanner/ScannerScreen.kt +++ b/app/src/main/java/com/getcode/view/main/scanner/ScannerScreen.kt @@ -63,14 +63,14 @@ import com.getcode.navigation.screens.EnterTipModal import com.getcode.navigation.screens.GetKinModal import com.getcode.navigation.screens.GiveKinModal import com.getcode.navigation.screens.ShareDownloadLinkModal -import com.getcode.ui.components.FullScreenProgressSpinner import com.getcode.ui.components.OnLifecycleEvent -import com.getcode.ui.components.PermissionCheck +import com.getcode.ui.components.PermissionResult import com.getcode.ui.components.getPermissionLauncher +import com.getcode.ui.components.rememberPermissionChecker import com.getcode.ui.utils.AnimationUtils -import com.getcode.ui.utils.KeepScreenOn import com.getcode.ui.utils.ModalAnimationSpeed import com.getcode.ui.utils.measured +import com.getcode.util.launchAppSettings import com.getcode.view.login.notificationPermissionCheck import com.getcode.view.main.bill.BillManagementOptions import com.getcode.view.main.scanner.views.CameraDisabledView @@ -78,7 +78,7 @@ import com.getcode.view.main.scanner.camera.CodeScanner import com.getcode.view.main.bill.HomeBill import com.getcode.view.main.scanner.modals.LoginConfirmation import com.getcode.view.main.scanner.modals.PaymentConfirmation -import com.getcode.view.main.scanner.views.PermissionsBlockingView +import com.getcode.view.main.scanner.views.CameraPermissionsMissingView import com.getcode.view.main.scanner.modals.ReceivedKinConfirmation import com.getcode.view.main.scanner.modals.TipConfirmation import com.getcode.view.main.scanner.views.HomeRestricted @@ -335,24 +335,39 @@ private fun BillContainer( onStartCamera: () -> Unit, onAction: (UiElement) -> Unit, ) { - val onPermissionResult = - { isGranted: Boolean -> - session.onCameraPermissionChanged(isGranted = isGranted) + val context = LocalContext.current as Activity + val onPermissionResult = { result: PermissionResult -> + session.onCameraPermissionResult(result) + if (result == PermissionResult.ShouldShowRationale) { + TopBarManager.showMessage( + TopBarManager.TopBarMessage( + title = context.getString(R.string.action_allowCameraAccess), + message = context.getString(R.string.error_description_cameraAccessRequired), + type = TopBarManager.TopBarMessageType.ERROR, + secondaryText = context.getString(R.string.action_openSettings), + secondaryAction = { context.launchAppSettings() } + ) + ) } + } - val launcher = getPermissionLauncher(onPermissionResult) - val context = LocalContext.current as Activity + val cameraPermissionLauncher = getPermissionLauncher(Manifest.permission.CAMERA, onPermissionResult) - SideEffect { - PermissionCheck.requestPermission( - context = context, + val permissionChecker = rememberPermissionChecker() + + val checkPermission = { shouldRequest: Boolean -> + permissionChecker.request( permission = Manifest.permission.CAMERA, - shouldRequest = false, + shouldRequest = shouldRequest, onPermissionResult = onPermissionResult, - launcher = launcher + launcher = cameraPermissionLauncher ) } + SideEffect { + checkPermission(false) + } + Box( modifier = Modifier .fillMaxSize() @@ -376,11 +391,9 @@ private fun BillContainer( } else -> { - PermissionsBlockingView( + CameraPermissionsMissingView( modifier = Modifier.fillMaxSize(), - context = context, - onPermissionResult = onPermissionResult, - launcher = launcher + onClick = { checkPermission(true) } ) } } diff --git a/app/src/main/java/com/getcode/view/main/scanner/views/PermissionsBlockingView.kt b/app/src/main/java/com/getcode/view/main/scanner/views/CameraPermissionsMissingView.kt similarity index 71% rename from app/src/main/java/com/getcode/view/main/scanner/views/PermissionsBlockingView.kt rename to app/src/main/java/com/getcode/view/main/scanner/views/CameraPermissionsMissingView.kt index e2a11de5..394a2f13 100644 --- a/app/src/main/java/com/getcode/view/main/scanner/views/PermissionsBlockingView.kt +++ b/app/src/main/java/com/getcode/view/main/scanner/views/CameraPermissionsMissingView.kt @@ -1,7 +1,5 @@ package com.getcode.view.main.scanner.views -import android.Manifest -import android.content.Context import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -21,15 +19,11 @@ import com.getcode.R import com.getcode.theme.CodeTheme import com.getcode.ui.components.ButtonState import com.getcode.ui.components.CodeButton -import com.getcode.ui.components.PermissionCheck -import com.getcode.ui.components.PermissionsLauncher @Composable -internal fun PermissionsBlockingView( +internal fun CameraPermissionsMissingView( modifier: Modifier = Modifier, - context: Context, - onPermissionResult: (Boolean) -> Unit, - launcher: PermissionsLauncher, + onClick: () -> Unit, ) { Box( modifier = modifier.background(Color.Black), @@ -44,15 +38,7 @@ internal fun PermissionsBlockingView( text = stringResource(R.string.subtitle_allowCameraAccess) ) CodeButton( - onClick = { - PermissionCheck.requestPermission( - context = context, - permission = Manifest.permission.CAMERA, - shouldRequest = true, - onPermissionResult = onPermissionResult, - launcher = launcher - ) - }, + onClick = onClick, modifier = Modifier.align(Alignment.CenterHorizontally), contentPadding = PaddingValues(), text = stringResource(id = R.string.action_allowCameraAccess),