Skip to content

Commit

Permalink
Copy auth code on item click (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
SaintPatrck authored Apr 16, 2024
1 parent 6d4df64 commit 8da98f9
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -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,
)
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -84,7 +86,7 @@ class ItemListingViewModel @Inject constructor(
}

is ItemListingAction.ItemClick -> {
sendEvent(ItemListingEvent.NavigateToEditItem(action.id))
handleItemClick(action)
}

is ItemListingAction.DialogDismiss -> {
Expand All @@ -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 -> {
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -50,7 +45,6 @@ fun VaultVerificationCodeItem(
timeLeftSeconds: Int,
alertThresholdSeconds: Int,
startIcon: IconData,
onCopyClick: () -> Unit,
onItemClick: () -> Unit,
modifier: Modifier = Modifier,
supportingLabel: String? = null,
Expand Down Expand Up @@ -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),
)
}
}
}

Expand All @@ -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"
)
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>

0 comments on commit 8da98f9

Please sign in to comment.