From 39e285fff8bbe0a8186ac93ded48005bfa613d99 Mon Sep 17 00:00:00 2001 From: Oleg Semenenko <146032743+oleg-livefront@users.noreply.github.com> Date: Tue, 19 Dec 2023 13:27:35 -0600 Subject: [PATCH] BIT-1076 Requesting Camera Permission (#415) --- app/src/main/AndroidManifest.xml | 5 ++ .../platform/base/util/PermissionsManager.kt | 30 ++++++++++++ .../base/util/PermissionsManagerImpl.kt | 37 +++++++++++++++ .../feature/additem/AddEditItemContent.kt | 26 +++++++++++ .../feature/additem/AddEditLoginItems.kt | 5 +- .../feature/additem/VaultAddItemScreen.kt | 6 +++ .../feature/additem/VaultAddItemViewModel.kt | 27 +++++++---- .../additem/VaultAddLoginItemTypeHandlers.kt | 8 ++-- .../base/util/FakePermissionManager.kt | 46 +++++++++++++++++++ .../feature/additem/VaultAddItemScreenTest.kt | 45 +++++++++++++++++- .../additem/VaultAddItemViewModelTest.kt | 31 +++++++++++-- 11 files changed, 247 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/PermissionsManager.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/PermissionsManagerImpl.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/platform/base/util/FakePermissionManager.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eea280c28..ae742ca4d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,11 @@ + + + Unit): ManagedActivityResultLauncher + + /** + * Method for checking whether the permission is granted. + */ + fun checkPermission(permission: String): Boolean + + /** + * Method for checking if an informative UI should be shown the user. + */ + fun shouldShouldRequestPermissionRationale( + permission: String, + ): Boolean +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/PermissionsManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/PermissionsManagerImpl.kt new file mode 100644 index 000000000..08d9fe6ba --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/PermissionsManagerImpl.kt @@ -0,0 +1,37 @@ +package com.x8bit.bitwarden.ui.platform.base.util + +import android.app.Activity +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.core.content.ContextCompat + +/** + * Primary implementation of [PermissionsManager]. + */ +class PermissionsManagerImpl( + private val activity: Activity, +) : PermissionsManager { + + @Composable + override fun getLauncher( + onResult: (Boolean) -> Unit, + ): ManagedActivityResultLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult, + ) + + override fun checkPermission(permission: String): Boolean = + ContextCompat.checkSelfPermission( + activity, + permission, + ) == PackageManager.PERMISSION_GRANTED + + override fun shouldShouldRequestPermissionRationale( + permission: String, + ): Boolean = + activity.shouldShowRequestPermissionRationale(permission) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/AddEditItemContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/AddEditItemContent.kt index 0b6b46069..b7d13ac1e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/AddEditItemContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/AddEditItemContent.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.vault.feature.additem +import android.Manifest import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -11,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.PermissionsManager import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.BitwardenMultiSelectButton import kotlinx.collections.immutable.toImmutableList @@ -19,6 +21,7 @@ import kotlinx.collections.immutable.toImmutableList * The top level content UI state for the [VaultAddItemScreen]. */ @Composable +@Suppress("LongMethod") fun AddEditItemContent( state: VaultAddItemState.ViewState.Content, isAddItemMode: Boolean, @@ -26,7 +29,23 @@ fun AddEditItemContent( commonTypeHandlers: VaultAddItemCommonTypeHandlers, loginItemTypeHandlers: VaultAddLoginItemTypeHandlers, modifier: Modifier = Modifier, + permissionsManager: PermissionsManager, ) { + val launcher = permissionsManager.getLauncher( + onResult = { isGranted -> + when (state.type) { + is VaultAddItemState.ViewState.Content.ItemType.SecureNotes -> Unit + // TODO: Create UI for card-type item creation BIT-507 + is VaultAddItemState.ViewState.Content.ItemType.Card -> Unit + // TODO: Create UI for identity-type item creation BIT-667 + is VaultAddItemState.ViewState.Content.ItemType.Identity -> Unit + is VaultAddItemState.ViewState.Content.ItemType.Login -> { + loginItemTypeHandlers.onSetupTotpClick(isGranted) + } + } + }, + ) + LazyColumn( modifier = modifier, ) { @@ -57,6 +76,13 @@ fun AddEditItemContent( isAddItemMode = isAddItemMode, commonActionHandler = commonTypeHandlers, loginItemTypeHandlers = loginItemTypeHandlers, + onTotpSetupClick = { + if (permissionsManager.checkPermission(Manifest.permission.CAMERA)) { + loginItemTypeHandlers.onSetupTotpClick(true) + } else { + launcher.launch(Manifest.permission.CAMERA) + } + }, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/AddEditLoginItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/AddEditLoginItems.kt index 08456aaae..babe9bdf2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/AddEditLoginItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/AddEditLoginItems.kt @@ -32,13 +32,14 @@ import kotlinx.collections.immutable.toImmutableList /** * The UI for adding and editing a login cipher. */ -@Suppress("LongMethod") +@Suppress("LongMethod", "LongParameterList") fun LazyListScope.addEditLoginItems( commonState: VaultAddItemState.ViewState.Content.Common, loginState: VaultAddItemState.ViewState.Content.ItemType.Login, isAddItemMode: Boolean, commonActionHandler: VaultAddItemCommonTypeHandlers, loginItemTypeHandlers: VaultAddLoginItemTypeHandlers, + onTotpSetupClick: () -> Unit, ) { item { Spacer(modifier = Modifier.height(8.dp)) @@ -112,7 +113,7 @@ fun LazyListScope.addEditLoginItems( BitwardenFilledTonalButtonWithIcon( label = stringResource(id = R.string.setup_totp), icon = painterResource(id = R.drawable.ic_light_bulb), - onClick = loginItemTypeHandlers.onSetupTotpClick, + onClick = onTotpSetupClick, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemScreen.kt index e54d81585..3633eb3ed 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemScreen.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.vault.feature.additem +import android.app.Activity import android.widget.Toast import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding @@ -27,6 +28,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState +import com.x8bit.bitwarden.ui.platform.base.util.PermissionsManager +import com.x8bit.bitwarden.ui.platform.base.util.PermissionsManagerImpl /** * Top level composable for the vault add item screen. @@ -37,6 +40,8 @@ import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState fun VaultAddItemScreen( onNavigateBack: () -> Unit, viewModel: VaultAddItemViewModel = hiltViewModel(), + permissionsManager: PermissionsManager = + PermissionsManagerImpl(LocalContext.current as Activity), ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() val context = LocalContext.current @@ -101,6 +106,7 @@ fun VaultAddItemScreen( }, loginItemTypeHandlers = loginItemTypeHandlers, commonTypeHandlers = commonTypeHandlers, + permissionsManager = permissionsManager, modifier = Modifier .imePadding() .padding(innerPadding) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModel.kt index 8215e0814..6d073c95e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModel.kt @@ -15,8 +15,8 @@ import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.concat import com.x8bit.bitwarden.ui.vault.feature.additem.model.CustomFieldType -import com.x8bit.bitwarden.ui.vault.feature.additem.util.toViewState import com.x8bit.bitwarden.ui.vault.feature.additem.model.toCustomField +import com.x8bit.bitwarden.ui.vault.feature.additem.util.toViewState import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType @@ -332,7 +332,7 @@ class VaultAddItemViewModel @Inject constructor( } is VaultAddItemAction.ItemType.LoginType.SetupTotpClick -> { - handleLoginSetupTotpClick() + handleLoginSetupTotpClick(action) } is VaultAddItemAction.ItemType.LoginType.UriSettingsClick -> { @@ -399,11 +399,18 @@ class VaultAddItemViewModel @Inject constructor( } } - private fun handleLoginSetupTotpClick() { + private fun handleLoginSetupTotpClick( + action: VaultAddItemAction.ItemType.LoginType.SetupTotpClick, + ) { viewModelScope.launch { + val message = if (action.isGranted) { + "Permission Granted, QR Code Scanner Not Implemented" + } else { + "Permission Not Granted, Manual QR Code Entry Not Implemented" + } sendEvent( event = VaultAddItemEvent.ShowToast( - message = "Setup TOTP", + message = message, ), ) } @@ -972,6 +979,13 @@ sealed class VaultAddItemAction { */ data class UriTextChange(val uri: String) : LoginType() + /** + * Represents the action to set up TOTP. + * + * @property isGranted the status of the camera permission + */ + data class SetupTotpClick(val isGranted: Boolean) : LoginType() + /** * Represents the action to open the username generator. */ @@ -987,11 +1001,6 @@ sealed class VaultAddItemAction { */ data object OpenPasswordGeneratorClick : LoginType() - /** - * Represents the action to set up TOTP. - */ - data object SetupTotpClick : LoginType() - /** * Represents the action of clicking TOTP settings */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddLoginItemTypeHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddLoginItemTypeHandlers.kt index 010478fa3..5a0a301ea 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddLoginItemTypeHandlers.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddLoginItemTypeHandlers.kt @@ -26,7 +26,7 @@ class VaultAddLoginItemTypeHandlers( val onOpenUsernameGeneratorClick: () -> Unit, val onPasswordCheckerClick: () -> Unit, val onOpenPasswordGeneratorClick: () -> Unit, - val onSetupTotpClick: () -> Unit, + val onSetupTotpClick: (Boolean) -> Unit, val onUriSettingsClick: () -> Unit, val onAddNewUriClick: () -> Unit, ) { @@ -72,8 +72,10 @@ class VaultAddLoginItemTypeHandlers( VaultAddItemAction.ItemType.LoginType.OpenPasswordGeneratorClick, ) }, - onSetupTotpClick = { - viewModel.trySendAction(VaultAddItemAction.ItemType.LoginType.SetupTotpClick) + onSetupTotpClick = { isGranted -> + viewModel.trySendAction( + VaultAddItemAction.ItemType.LoginType.SetupTotpClick(isGranted), + ) }, onUriSettingsClick = { viewModel.trySendAction(VaultAddItemAction.ItemType.LoginType.UriSettingsClick) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/util/FakePermissionManager.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/util/FakePermissionManager.kt new file mode 100644 index 000000000..4f56b89ff --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/util/FakePermissionManager.kt @@ -0,0 +1,46 @@ +package com.x8bit.bitwarden.ui.platform.base.util + +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.compose.runtime.Composable +import io.mockk.every +import io.mockk.mockk + +/** + * A helper class used to test permissions + */ +class FakePermissionManager : PermissionsManager { + + /** + * The value returned when we check if we have the permission. + */ + var checkPermissionResult: Boolean = false + + /** + * The value returned when the user is asked for permission. + */ + var getPermissionsResult: Boolean = false + + /** + * * The value for whether a rationale should be shown to the user. + */ + var shouldShowRequestRationale: Boolean = false + + @Composable + override fun getLauncher( + onResult: (Boolean) -> Unit, + ): ManagedActivityResultLauncher { + return mockk { + every { launch(any()) } answers { onResult.invoke(getPermissionsResult) } + } + } + + override fun checkPermission(permission: String): Boolean { + return checkPermissionResult + } + + override fun shouldShouldRequestPermissionRationale( + permission: String, + ): Boolean { + return shouldShowRequestRationale + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemScreenTest.kt index b71a7ea15..2d4a9382e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemScreenTest.kt @@ -30,6 +30,7 @@ import com.x8bit.bitwarden.ui.util.onAllNodesWithTextAfterScroll import com.x8bit.bitwarden.ui.util.onNodeWithContentDescriptionAfterScroll import com.x8bit.bitwarden.ui.util.onNodeWithTextAfterScroll import com.x8bit.bitwarden.ui.vault.feature.additem.model.CustomFieldType +import com.x8bit.bitwarden.ui.platform.base.util.FakePermissionManager import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType import io.mockk.every import io.mockk.mockk @@ -49,6 +50,8 @@ class VaultAddItemScreenTest : BaseComposeTest() { private val mutableEventFlow = MutableSharedFlow(Int.MAX_VALUE) private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE_LOGIN) + private val fakePermissionManager: FakePermissionManager = FakePermissionManager() + private val viewModel = mockk(relaxed = true) { every { eventFlow } returns mutableEventFlow every { stateFlow } returns mutableStateFlow @@ -60,6 +63,7 @@ class VaultAddItemScreenTest : BaseComposeTest() { VaultAddItemScreen( viewModel = viewModel, onNavigateBack = { onNavigateBackCalled = true }, + permissionsManager = fakePermissionManager, ) } } @@ -315,15 +319,52 @@ class VaultAddItemScreenTest : BaseComposeTest() { .assertTextContains("•••••••••••") } + @Suppress("MaxLineLength") @Test - fun `in ItemType_Login state clicking Set up TOTP button should trigger SetupTotpClick`() { + fun `in ItemType_Login state clicking SetupTOTP button with a positive result should send true if permission check returns true`() { + fakePermissionManager.checkPermissionResult = true + composeTestRule .onNodeWithTextAfterScroll(text = "Set up TOTP") .performClick() verify { viewModel.trySendAction( - VaultAddItemAction.ItemType.LoginType.SetupTotpClick, + VaultAddItemAction.ItemType.LoginType.SetupTotpClick(true), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in ItemType_Login state clicking SetupTOTP button with a positive result should send true`() { + fakePermissionManager.checkPermissionResult = false + fakePermissionManager.getPermissionsResult = true + + composeTestRule + .onNodeWithTextAfterScroll(text = "Set up TOTP") + .performClick() + + verify { + viewModel.trySendAction( + VaultAddItemAction.ItemType.LoginType.SetupTotpClick(true), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in ItemType_Login state clicking Set up TOTP button with a negative result should send false`() { + fakePermissionManager.checkPermissionResult = false + fakePermissionManager.getPermissionsResult = false + + composeTestRule + .onNodeWithTextAfterScroll(text = "Set up TOTP") + .performClick() + + verify { + viewModel.trySendAction( + VaultAddItemAction.ItemType.LoginType.SetupTotpClick(false), ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModelTest.kt index 81d9f112b..27bd463fe 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/additem/VaultAddItemViewModelTest.kt @@ -474,12 +474,37 @@ class VaultAddItemViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `SetupTotpClick should emit ShowToast with 'Setup TOTP' message`() = runTest { + fun `SetupTotpClick should emit ShowToast with permission granted when isGranted is true`() = runTest { val viewModel = createAddVaultItemViewModel() viewModel.eventFlow.test { - viewModel.actionChannel.trySend(VaultAddItemAction.ItemType.LoginType.SetupTotpClick) - assertEquals(VaultAddItemEvent.ShowToast("Setup TOTP"), awaitItem()) + viewModel.actionChannel.trySend( + VaultAddItemAction.ItemType.LoginType.SetupTotpClick( + true, + ), + ) + assertEquals( + VaultAddItemEvent.ShowToast("Permission Granted, QR Code Scanner Not Implemented"), + awaitItem(), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `SetupTotpClick should emit ShowToast with permission not granted when isGranted is false`() = runTest { + val viewModel = createAddVaultItemViewModel() + + viewModel.eventFlow.test { + viewModel.actionChannel.trySend( + VaultAddItemAction.ItemType.LoginType.SetupTotpClick( + false, + ), + ) + assertEquals( + VaultAddItemEvent.ShowToast("Permission Not Granted, Manual QR Code Entry Not Implemented"), + awaitItem(), + ) } }