Copy auth code on item click (#28)

This commit is contained in:
Patrick Honkonen
2024-04-15 20:39:45 -04:00
committed by GitHub
parent 6d4df646af
commit 8da98f95e1
8 changed files with 175 additions and 35 deletions

View File

@@ -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,
)
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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()

View File

@@ -169,20 +169,21 @@ fun ItemListingScreen(
LazyColumn {
items(currentState.itemList) {
VaultVerificationCodeItem(
authCode = it.authCode,
issuer = it.issuer,
periodSeconds = it.periodSeconds,
timeLeftSeconds = it.timeLeftSeconds,
alertThresholdSeconds = it.alertThresholdSeconds,
startIcon = it.startIcon,
onItemClick = {
viewModel.trySendAction(
ItemListingAction.ItemClick(it.authCode)
)
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
startIcon = it.startIcon,
issuer = it.issuer,
supportingLabel = it.supportingLabel,
timeLeftSeconds = it.timeLeftSeconds,
periodSeconds = it.periodSeconds,
alertThresholdSeconds = it.alertThresholdSeconds,
authCode = it.authCode,
onCopyClick = { /*TODO*/ },
onItemClick = {
onNavigateToEditItemScreen(it.id)
},
)
}
}

View File

@@ -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<ItemListingState, ItemListingEvent, ItemListingAction>(
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.

View File

@@ -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"
)
}
}

View File

@@ -86,4 +86,5 @@
<string name="unique_codes">Uniqe codes</string>
<string name="help">Help</string>
<string name="tutorial">Tutorial</string>
<string name="value_has_been_copied">%1$s copied</string>
</resources>