From 8da98f95e112109f90fcc99d1dc53c5c9bf15d32 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Mon, 15 Apr 2024 20:39:45 -0400 Subject: [PATCH] Copy auth code on item click (#28) --- .../clipboard/BitwardenClipboardManager.kt | 42 +++++++++++ .../BitwardenClipboardManagerImpl.kt | 71 +++++++++++++++++++ .../manager/clipboard/ClearClipboardWorker.kt | 22 ++++++ .../manager/di/PlatformManagerModule.kt | 10 +++ .../feature/itemlisting/ItemListingScreen.kt | 19 ++--- .../itemlisting/ItemListingViewModel.kt | 15 +++- .../itemlisting/VaultVerificationCodeItem.kt | 28 ++------ app/src/main/res/values/strings.xml | 1 + 8 files changed, 174 insertions(+), 34 deletions(-) create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/clipboard/BitwardenClipboardManager.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/clipboard/BitwardenClipboardManagerImpl.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/clipboard/ClearClipboardWorker.kt diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/clipboard/BitwardenClipboardManager.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/clipboard/BitwardenClipboardManager.kt new file mode 100644 index 000000000..f5bce6aba --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/clipboard/BitwardenClipboardManager.kt @@ -0,0 +1,42 @@ +package com.x8bit.bitwarden.authenticator.data.platform.manager.clipboard + +import androidx.compose.ui.text.AnnotatedString +import com.x8bit.bitwarden.authenticator.ui.platform.base.util.Text + +/** + * Wrapper class for using the clipboard. + */ +interface BitwardenClipboardManager { + + /** + * Places the given [text] into the device's clipboard. Setting the data to [isSensitive] will + * obfuscate the displayed data on the default popup (true by default). A toast will be + * displayed on devices that do not have a default popup (pre-API 32) and will not be displayed + * on newer APIs. If a toast is displayed, it will be formatted as "[text] copied" or if a + * [toastDescriptorOverride] is provided, it will be formatted as + * "[toastDescriptorOverride] copied". + */ + fun setText( + text: AnnotatedString, + isSensitive: Boolean = true, + toastDescriptorOverride: String? = null, + ) + + /** + * See [setText] for more details. + */ + fun setText( + text: String, + isSensitive: Boolean = true, + toastDescriptorOverride: String? = null, + ) + + /** + * See [setText] for more details. + */ + fun setText( + text: Text, + isSensitive: Boolean = true, + toastDescriptorOverride: String? = null, + ) +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/clipboard/BitwardenClipboardManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/clipboard/BitwardenClipboardManagerImpl.kt new file mode 100644 index 000000000..f0c72b133 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/clipboard/BitwardenClipboardManagerImpl.kt @@ -0,0 +1,71 @@ +package com.x8bit.bitwarden.authenticator.data.platform.manager.clipboard + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Build +import android.widget.Toast +import androidx.compose.ui.text.AnnotatedString +import androidx.core.content.getSystemService +import androidx.core.os.persistableBundleOf +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import com.x8bit.bitwarden.authenticator.R +import com.x8bit.bitwarden.authenticator.ui.platform.base.util.Text +import com.x8bit.bitwarden.authenticator.ui.platform.base.util.toAnnotatedString +import com.x8bit.bitwarden.data.platform.manager.clipboard.ClearClipboardWorker + +/** + * Default implementation of the [BitwardenClipboardManager] interface. + */ +class BitwardenClipboardManagerImpl( + private val context: Context, +) : BitwardenClipboardManager { + private val clipboardManager: ClipboardManager = requireNotNull(context.getSystemService()) + + override fun setText( + text: AnnotatedString, + isSensitive: Boolean, + toastDescriptorOverride: String?, + ) { + clipboardManager.setPrimaryClip( + ClipData + .newPlainText("", text) + .apply { + description.extras = persistableBundleOf( + "android.content.extra.IS_SENSITIVE" to isSensitive, + ) + }, + ) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + val descriptor = toastDescriptorOverride ?: text + Toast + .makeText( + context, + context.resources.getString(R.string.value_has_been_copied, descriptor), + Toast.LENGTH_SHORT, + ) + .show() + } + + val clearClipboardRequest: OneTimeWorkRequest = + OneTimeWorkRequest + .Builder(ClearClipboardWorker::class.java) + .build() + + WorkManager.getInstance(context).enqueueUniqueWork( + "ClearClipboard", + ExistingWorkPolicy.REPLACE, + clearClipboardRequest, + ) + } + + override fun setText(text: String, isSensitive: Boolean, toastDescriptorOverride: String?) { + setText(text.toAnnotatedString(), isSensitive, toastDescriptorOverride) + } + + override fun setText(text: Text, isSensitive: Boolean, toastDescriptorOverride: String?) { + setText(text.toString(context.resources), isSensitive, toastDescriptorOverride) + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/clipboard/ClearClipboardWorker.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/clipboard/ClearClipboardWorker.kt new file mode 100644 index 000000000..6803f4016 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/clipboard/ClearClipboardWorker.kt @@ -0,0 +1,22 @@ +package com.x8bit.bitwarden.data.platform.manager.clipboard + +import android.content.ClipboardManager +import android.content.Context +import android.content.Context.CLIPBOARD_SERVICE +import androidx.work.Worker +import androidx.work.WorkerParameters + +/** + * A worker to clear the clipboard manager. + */ +class ClearClipboardWorker(appContext: Context, workerParams: WorkerParameters) : + Worker(appContext, workerParams) { + + private val clipboardManager = + appContext.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + + override fun doWork(): Result { + clipboardManager.clearPrimaryClip() + return Result.success() + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/di/PlatformManagerModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/di/PlatformManagerModule.kt index 99287aaf2..dfbda2dbd 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/di/PlatformManagerModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/data/platform/manager/di/PlatformManagerModule.kt @@ -1,12 +1,16 @@ package com.x8bit.bitwarden.authenticator.data.platform.manager.di +import android.content.Context import com.x8bit.bitwarden.authenticator.data.platform.manager.DispatcherManager import com.x8bit.bitwarden.authenticator.data.platform.manager.DispatcherManagerImpl import com.x8bit.bitwarden.authenticator.data.platform.manager.SdkClientManager import com.x8bit.bitwarden.authenticator.data.platform.manager.SdkClientManagerImpl +import com.x8bit.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager +import com.x8bit.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManagerImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import java.time.Clock import javax.inject.Singleton @@ -18,6 +22,12 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object PlatformManagerModule { + @Provides + @Singleton + fun provideBitwardenClipboardManager( + @ApplicationContext context: Context, + ): BitwardenClipboardManager = BitwardenClipboardManagerImpl(context) + @Provides @Singleton fun provideBitwardenDispatchers(): DispatcherManager = DispatcherManagerImpl() diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt index bc11efc78..0038e19bd 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt @@ -169,20 +169,21 @@ fun ItemListingScreen( LazyColumn { items(currentState.itemList) { VaultVerificationCodeItem( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - startIcon = it.startIcon, + authCode = it.authCode, issuer = it.issuer, - supportingLabel = it.supportingLabel, - timeLeftSeconds = it.timeLeftSeconds, periodSeconds = it.periodSeconds, + timeLeftSeconds = it.timeLeftSeconds, alertThresholdSeconds = it.alertThresholdSeconds, - authCode = it.authCode, - onCopyClick = { /*TODO*/ }, + startIcon = it.startIcon, onItemClick = { - onNavigateToEditItemScreen(it.id) + viewModel.trySendAction( + ItemListingAction.ItemClick(it.authCode) + ) }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + supportingLabel = it.supportingLabel, ) } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt index 619bf954d..e8fce7f6e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt @@ -11,6 +11,7 @@ import com.x8bit.bitwarden.authenticator.data.authenticator.manager.model.Verifi import com.x8bit.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository import com.x8bit.bitwarden.authenticator.data.authenticator.repository.model.CreateItemResult import com.x8bit.bitwarden.authenticator.data.authenticator.repository.model.TotpCodeResult +import com.x8bit.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.authenticator.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.authenticator.data.platform.repository.model.DataState import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util.toViewState @@ -35,6 +36,7 @@ import javax.inject.Inject @HiltViewModel class ItemListingViewModel @Inject constructor( private val authenticatorRepository: AuthenticatorRepository, + private val clipboardManager: BitwardenClipboardManager, settingsRepository: SettingsRepository, ) : BaseViewModel( initialState = ItemListingState( @@ -84,7 +86,7 @@ class ItemListingViewModel @Inject constructor( } is ItemListingAction.ItemClick -> { - sendEvent(ItemListingEvent.NavigateToEditItem(action.id)) + handleItemClick(action) } is ItemListingAction.DialogDismiss -> { @@ -97,6 +99,15 @@ class ItemListingViewModel @Inject constructor( } } + private fun handleItemClick(action: ItemListingAction.ItemClick) { + clipboardManager.setText(action.authCode) + sendEvent( + ItemListingEvent.ShowToast( + message = R.string.value_has_been_copied.asText(action.authCode) + ) + ) + } + private fun handleInternalAction(internalAction: ItemListingAction.Internal) { when (internalAction) { is ItemListingAction.Internal.AuthCodesUpdated -> { @@ -479,7 +490,7 @@ sealed class ItemListingAction { /** * The user clicked a list item. */ - data class ItemClick(val id: String) : ItemListingAction() + data class ItemClick(val authCode: String) : ItemListingAction() /** * The user dismissed the dialog. diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/VaultVerificationCodeItem.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/VaultVerificationCodeItem.kt index eccf1b4d2..7de350d6d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/VaultVerificationCodeItem.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/VaultVerificationCodeItem.kt @@ -9,16 +9,12 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -36,7 +32,6 @@ import com.x8bit.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme * @param periodSeconds The times span where the code is valid. * @param timeLeftSeconds The seconds remaining until a new code is needed. * @param startIcon The leading icon for the item. - * @param onCopyClick The lambda function to be invoked when the copy button is clicked. * @param onItemClick The lambda function to be invoked when the item is clicked. * @param modifier The modifier for the item. * @param supportingLabel The supporting label for the item. @@ -50,7 +45,6 @@ fun VaultVerificationCodeItem( timeLeftSeconds: Int, alertThresholdSeconds: Int, startIcon: IconData, - onCopyClick: () -> Unit, onItemClick: () -> Unit, modifier: Modifier = Modifier, supportingLabel: String? = null, @@ -112,17 +106,6 @@ fun VaultVerificationCodeItem( style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - - IconButton( - onClick = onCopyClick, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_copy), - contentDescription = stringResource(id = R.string.copy), - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp), - ) - } } } @@ -132,16 +115,15 @@ fun VaultVerificationCodeItem( private fun VerificationCodeItem_preview() { AuthenticatorTheme { VaultVerificationCodeItem( - startIcon = IconData.Local(R.drawable.ic_login_item), - issuer = "Sample Label", - supportingLabel = "Supporting Label", authCode = "1234567890".chunked(3).joinToString(" "), - timeLeftSeconds = 15, + issuer = "Sample Label", periodSeconds = 30, - onCopyClick = {}, + timeLeftSeconds = 15, + alertThresholdSeconds = 7, + startIcon = IconData.Local(R.drawable.ic_login_item), onItemClick = {}, modifier = Modifier.padding(horizontal = 16.dp), - alertThresholdSeconds = 7 + supportingLabel = "Supporting Label" ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13125310a..2da2acda0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -86,4 +86,5 @@ Uniqe codes Help Tutorial + %1$s copied