mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 09:56:45 -06:00
Add ViewAsQrCode first draft
This commit is contained in:
parent
f4f669683e
commit
225cb24ac1
@ -53,6 +53,8 @@ import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.navigateToVaultMo
|
||||
import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.vaultMoveToOrganizationDestination
|
||||
import com.x8bit.bitwarden.ui.vault.feature.qrcodescan.navigateToQrCodeScanScreen
|
||||
import com.x8bit.bitwarden.ui.vault.feature.qrcodescan.vaultQrCodeScanDestination
|
||||
import com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.navigateToViewAsQrCode
|
||||
import com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.viewAsQrCodeDestination
|
||||
|
||||
const val VAULT_UNLOCKED_GRAPH_ROUTE: String = "vault_unlocked_graph"
|
||||
|
||||
@ -165,6 +167,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||
passwordHistoryMode = GeneratorPasswordHistoryMode.Item(itemId = it),
|
||||
)
|
||||
},
|
||||
onNavigateToViewAsQrCode = { navController.navigateToViewAsQrCode(it) },
|
||||
)
|
||||
vaultQrCodeScanDestination(
|
||||
onNavigateToManualCodeEntryScreen = {
|
||||
@ -228,5 +231,8 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||
onNavigateBackToVault = { navController.navigateToVaultUnlockedGraph() },
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
viewAsQrCodeDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import androidx.navigation.navArgument
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
|
||||
import com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.ViewAsQrCodeArgs
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
|
||||
private const val LOGIN: String = "login"
|
||||
@ -47,12 +48,18 @@ fun NavGraphBuilder.vaultItemDestination(
|
||||
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
|
||||
onNavigateToAttachments: (vaultItemId: String) -> Unit,
|
||||
onNavigateToPasswordHistory: (vaultItemId: String) -> Unit,
|
||||
onNavigateToViewAsQrCode: (args: ViewAsQrCodeArgs) -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = VAULT_ITEM_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(VAULT_ITEM_ID) { type = NavType.StringType },
|
||||
navArgument(VAULT_ITEM_CIPHER_TYPE) { type = NavType.StringType },
|
||||
navArgument(VAULT_ITEM_ID) {
|
||||
type = NavType.StringType
|
||||
},
|
||||
navArgument(VAULT_ITEM_CIPHER_TYPE) {
|
||||
type = NavType.StringType
|
||||
defaultValue = LOGIN
|
||||
},
|
||||
),
|
||||
) {
|
||||
VaultItemScreen(
|
||||
@ -61,6 +68,7 @@ fun NavGraphBuilder.vaultItemDestination(
|
||||
onNavigateToMoveToOrganization = onNavigateToMoveToOrganization,
|
||||
onNavigateToAttachments = onNavigateToAttachments,
|
||||
onNavigateToPasswordHistory = onNavigateToPasswordHistory,
|
||||
onNavigateToViewAsQrCode = onNavigateToViewAsQrCode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +46,9 @@ import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHan
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.ViewAsQrCodeArgs
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
|
||||
/**
|
||||
* Displays the vault item screen.
|
||||
@ -62,6 +64,7 @@ fun VaultItemScreen(
|
||||
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
|
||||
onNavigateToAttachments: (vaultItemId: String) -> Unit,
|
||||
onNavigateToPasswordHistory: (vaultItemId: String) -> Unit,
|
||||
onNavigateToViewAsQrCode: (args: ViewAsQrCodeArgs) -> Unit,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
@ -100,6 +103,15 @@ fun VaultItemScreen(
|
||||
|
||||
is VaultItemEvent.NavigateToUri -> intentManager.launchUri(event.uri.toUri())
|
||||
|
||||
is VaultItemEvent.NavigateToViewAsQrCode -> {
|
||||
onNavigateToViewAsQrCode(
|
||||
ViewAsQrCodeArgs(
|
||||
vaultItemId = event.itemId,
|
||||
vaultItemCipherType = event.type,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is VaultItemEvent.NavigateToAttachments -> onNavigateToAttachments(event.itemId)
|
||||
|
||||
is VaultItemEvent.NavigateToMoveToOrganization -> {
|
||||
@ -182,6 +194,16 @@ fun VaultItemScreen(
|
||||
}
|
||||
BitwardenOverflowActionItem(
|
||||
menuItemDataList = persistentListOfNotNull(
|
||||
OverflowMenuItemData(
|
||||
text = stringResource(id = R.string.view_as_qr_code),
|
||||
onClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Common.ViewAsQrCodeClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
OverflowMenuItemData(
|
||||
text = stringResource(id = R.string.attachments),
|
||||
onClick = remember(viewModel) {
|
||||
|
||||
@ -228,6 +228,7 @@ class VaultItemViewModel @Inject constructor(
|
||||
handleNoAttachmentFileLocationReceive()
|
||||
}
|
||||
|
||||
is VaultItemAction.Common.ViewAsQrCodeClick -> handleViewAsQrCodeClick()
|
||||
is VaultItemAction.Common.AttachmentsClick -> handleAttachmentsClick()
|
||||
is VaultItemAction.Common.CloneClick -> handleCloneClick()
|
||||
is VaultItemAction.Common.MoveToOrganizationClick -> handleMoveToOrganizationClick()
|
||||
@ -439,6 +440,16 @@ class VaultItemViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleViewAsQrCodeClick() {
|
||||
// TODO - do we need onContent?
|
||||
sendEvent(
|
||||
event = VaultItemEvent.NavigateToViewAsQrCode(
|
||||
itemId = state.vaultItemId,
|
||||
type = state.cipherType,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleAttachmentsClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
@ -1927,6 +1938,14 @@ sealed class VaultItemEvent {
|
||||
val uri: String,
|
||||
) : VaultItemEvent()
|
||||
|
||||
/**
|
||||
* Navigate to view as QR code screen.
|
||||
*/
|
||||
data class NavigateToViewAsQrCode(
|
||||
val itemId: String,
|
||||
val type: VaultItemCipherType,
|
||||
) : VaultItemEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the attachments screen.
|
||||
*/
|
||||
@ -2098,6 +2117,11 @@ sealed class VaultItemAction {
|
||||
* The user has clicked the password history text.
|
||||
*/
|
||||
data object PasswordHistoryClick : Common()
|
||||
|
||||
/**
|
||||
* User clicked the View as QR code button.
|
||||
*/
|
||||
data object ViewAsQrCodeClick : Common()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.viewasqrcode
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
|
||||
private const val VAULT_ITEM_ID = "vault_item_id"
|
||||
|
||||
private const val LOGIN: String = "login"
|
||||
private const val CARD: String = "card"
|
||||
private const val IDENTITY: String = "identity"
|
||||
private const val SECURE_NOTE: String = "secure_note"
|
||||
private const val SSH_KEY: String = "ssh_key"
|
||||
private const val CIPHER_TYPE: String = "vault_item_type"
|
||||
|
||||
private const val VIEW_AS_QR_CODE_PREFIX: String = "view_as_qr_code"
|
||||
|
||||
private const val VIEW_AS_QR_CODE_ROUTE: String =
|
||||
VIEW_AS_QR_CODE_PREFIX +
|
||||
"/{$VAULT_ITEM_ID}" +
|
||||
"?$CIPHER_TYPE={$CIPHER_TYPE}"
|
||||
|
||||
/**
|
||||
* Class to retrieve view as QR code arguments from the [SavedStateHandle].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
data class ViewAsQrCodeArgs(
|
||||
val vaultItemId: String,
|
||||
val vaultItemCipherType: VaultItemCipherType,
|
||||
) {
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
vaultItemId = checkNotNull(savedStateHandle.get<String>(VAULT_ITEM_ID)),
|
||||
vaultItemCipherType = requireNotNull(savedStateHandle.get<String>(CIPHER_TYPE))
|
||||
.toVaultItemCipherType(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the view as QR code screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.viewAsQrCodeDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = VIEW_AS_QR_CODE_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(VAULT_ITEM_ID) { type = NavType.StringType },
|
||||
navArgument(CIPHER_TYPE) { type = NavType.StringType },
|
||||
),
|
||||
) {
|
||||
ViewAsQrCodeScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the view as QR code screen.
|
||||
*/
|
||||
fun NavController.navigateToViewAsQrCode(
|
||||
args: ViewAsQrCodeArgs,
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
this.navigate(
|
||||
route = "$VIEW_AS_QR_CODE_PREFIX/${args.vaultItemId}" +
|
||||
"?$CIPHER_TYPE=${args.vaultItemCipherType.toTypeString()}",
|
||||
navOptions = navOptions,
|
||||
)
|
||||
}
|
||||
|
||||
private fun VaultItemCipherType.toTypeString(): String =
|
||||
when (this) {
|
||||
VaultItemCipherType.LOGIN -> LOGIN
|
||||
VaultItemCipherType.CARD -> CARD
|
||||
VaultItemCipherType.IDENTITY -> IDENTITY
|
||||
VaultItemCipherType.SECURE_NOTE -> SECURE_NOTE
|
||||
VaultItemCipherType.SSH_KEY -> SSH_KEY
|
||||
}
|
||||
|
||||
private fun String.toVaultItemCipherType(): VaultItemCipherType =
|
||||
when (this) {
|
||||
LOGIN -> VaultItemCipherType.LOGIN
|
||||
CARD -> VaultItemCipherType.CARD
|
||||
IDENTITY -> VaultItemCipherType.IDENTITY
|
||||
SECURE_NOTE -> VaultItemCipherType.SECURE_NOTE
|
||||
SSH_KEY -> VaultItemCipherType.SSH_KEY
|
||||
else -> throw IllegalStateException(
|
||||
"Cipher Type string arguments for ViewAsQrCodeNavigation must match!",
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,369 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.viewasqrcode
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.vault.feature.attachments.handlers.AttachmentsHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.handlers.ViewAsQrCodeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.model.QrCodeType
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Displays the view as QR code screen.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ViewAsQrCodeScreen(
|
||||
viewModel: ViewAsQrCodeViewModel = hiltViewModel(),
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val viewAsQrCodeHandlers = remember(viewModel) { ViewAsQrCodeHandlers.create(viewModel) }
|
||||
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
ViewAsQrCodeEvent.NavigateBack -> onNavigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
BitwardenScaffold(
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.view_as_qr_code),
|
||||
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ViewAsQrCodeAction.BackClick) }
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) {
|
||||
when (val viewState = state.viewState) {
|
||||
is ViewAsQrCodeState.ViewState.Loading -> BitwardenLoadingContent(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
|
||||
is ViewAsQrCodeState.ViewState.Error -> BitwardenErrorContent(
|
||||
message = "ERROR",
|
||||
onTryAgainClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ViewAsQrCodeAction.BackClick) }
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
|
||||
is ViewAsQrCodeState.ViewState.Content -> {
|
||||
//TODO add ViewAsQrCodeContent
|
||||
val contentState = state
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
// QR Code display
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(250.dp)
|
||||
.background(Color.White)
|
||||
.padding(8.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
//TODO set qrcode image
|
||||
painter = rememberVectorPainter(id = R.drawable.bitwarden_logo),
|
||||
colorFilter = ColorFilter.tint(BitwardenTheme.colorScheme.icon.secondary),
|
||||
|
||||
//bitmap = contentState.qrCodeBitmap.asImageBitmap(),
|
||||
contentDescription = stringResource(id = R.string.qr_code),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
//
|
||||
// // QR Code type selector
|
||||
// BitwardenMultiSelectButton(
|
||||
// label = stringResource(id = R.string.qr_code_type),
|
||||
// options = contentState.qrCodeTypes.map { it.displayName() }.toImmutableList(),
|
||||
// selectedOption = contentState.selectedQrCodeType.displayName(),
|
||||
// onOptionSelected = { selectedOption ->
|
||||
// val selectedType = contentState.qrCodeTypes.first {
|
||||
// it.displayName() == selectedOption
|
||||
// }
|
||||
// viewModel.trySendAction(ViewAsQrCodeAction.QrCodeTypeSelect(selectedType))
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(16.dp))
|
||||
//
|
||||
// // Dynamic fields based on selected QR code type
|
||||
// when (contentState.selectedQrCodeType) {
|
||||
// QrCodeType.Text -> {
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.text),
|
||||
// value = contentState.fields["text"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("text", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// QrCodeType.Url -> {
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.url),
|
||||
// value = contentState.fields["url"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("url", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// QrCodeType.Email -> {
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.email),
|
||||
// value = contentState.fields["email"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("email", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(8.dp))
|
||||
//
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.subject),
|
||||
// value = contentState.fields["subject"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("subject", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(8.dp))
|
||||
//
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.body),
|
||||
// value = contentState.fields["body"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("body", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// QrCodeType.Phone -> {
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.phone),
|
||||
// value = contentState.fields["phone"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("phone", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// QrCodeType.SMS -> {
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.phone),
|
||||
// value = contentState.fields["phone"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("phone", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(8.dp))
|
||||
//
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.message),
|
||||
// value = contentState.fields["message"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("message", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// QrCodeType.WiFi -> {
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.ssid),
|
||||
// value = contentState.fields["ssid"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("ssid", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(8.dp))
|
||||
//
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.password),
|
||||
// value = contentState.fields["password"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("password", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(8.dp))
|
||||
//
|
||||
// BitwardenMultiSelectButton(
|
||||
// label = stringResource(id = R.string.encryption_type),
|
||||
// options = listOf("WPA", "WEP", "None").toImmutableList(),
|
||||
// selectedOption = contentState.fields["type"] ?: "WPA",
|
||||
// onOptionSelected = { selectedOption ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("type", selectedOption)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(8.dp))
|
||||
//
|
||||
// BitwardenMultiSelectButton(
|
||||
// label = stringResource(id = R.string.hidden),
|
||||
// options = listOf("true", "false").toImmutableList(),
|
||||
// selectedOption = contentState.fields["hidden"] ?: "false",
|
||||
// onOptionSelected = { selectedOption ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("hidden", selectedOption)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// QrCodeType.Contact -> {
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.name),
|
||||
// value = contentState.fields["name"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("name", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(8.dp))
|
||||
//
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.phone),
|
||||
// value = contentState.fields["phone"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("phone", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(8.dp))
|
||||
//
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.email),
|
||||
// value = contentState.fields["email"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("email", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(8.dp))
|
||||
//
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.organization),
|
||||
// value = contentState.fields["organization"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("organization", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(8.dp))
|
||||
//
|
||||
// BitwardenTextField(
|
||||
// label = stringResource(id = R.string.address),
|
||||
// value = contentState.fields["address"] ?: "",
|
||||
// onValueChange = { newValue ->
|
||||
// viewModel.trySendAction(
|
||||
// ViewAsQrCodeAction.FieldValueChange("address", newValue)
|
||||
// )
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,353 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.viewasqrcode
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
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.item.VaultItemArgs
|
||||
import com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.model.QrCodeConfig
|
||||
import com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.model.QrCodeType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.util.QrCodeGenerator
|
||||
import com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
/**
|
||||
* ViewModel responsible for handling user interactions in the attachments screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class ViewAsQrCodeViewModel @Inject constructor(
|
||||
private val vaultRepository: VaultRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<ViewAsQrCodeState, ViewAsQrCodeEvent, ViewAsQrCodeAction>(
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
val args = ViewAsQrCodeArgs(savedStateHandle)
|
||||
ViewAsQrCodeState(
|
||||
cipherId = args.vaultItemId,
|
||||
cipherType = args.vaultItemCipherType,
|
||||
viewState = ViewAsQrCodeState.ViewState.Loading,
|
||||
dialogState = null,
|
||||
)
|
||||
},
|
||||
) {
|
||||
private val args = ViewAsQrCodeArgs(savedStateHandle)
|
||||
|
||||
init {
|
||||
//TODO get args.vaultItemCipherType and auto-map
|
||||
vaultRepository
|
||||
.getVaultItemStateFlow(args.vaultItemId)
|
||||
.map { ViewAsQrCodeAction.Internal.CipherReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: ViewAsQrCodeAction) {
|
||||
when (action) {
|
||||
ViewAsQrCodeAction.BackClick -> handleBackClick()
|
||||
is ViewAsQrCodeAction.QrCodeTypeSelect -> handleQrCodeTypeSelect(action)
|
||||
is ViewAsQrCodeAction.FieldValueChange -> handleFieldValueChange(action)
|
||||
is ViewAsQrCodeAction.Internal.CipherReceive -> handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBackClick() {
|
||||
sendEvent(ViewAsQrCodeEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: ViewAsQrCodeAction.Internal) {
|
||||
when (action) {
|
||||
is ViewAsQrCodeAction.Internal.CipherReceive -> handleCipherReceive(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCipherReceive(action: ViewAsQrCodeAction.Internal.CipherReceive) {
|
||||
when (val dataState = action.cipherDataState) {
|
||||
is DataState.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = ViewAsQrCodeState.ViewState.Error(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is DataState.Loaded -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = dataState
|
||||
.data
|
||||
?.toViewState()
|
||||
?: ViewAsQrCodeState.ViewState.Error(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DataState.Loading -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = ViewAsQrCodeState.ViewState.Loading)
|
||||
}
|
||||
}
|
||||
|
||||
is DataState.NoNetwork -> mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = ViewAsQrCodeState.ViewState.Error(
|
||||
message = R.string.internet_connection_required_title
|
||||
.asText()
|
||||
.concat(
|
||||
" ".asText(),
|
||||
R.string.internet_connection_required_message.asText(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is DataState.Pending -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = dataState
|
||||
.data
|
||||
?.toViewState()
|
||||
?: ViewAsQrCodeState.ViewState.Error(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun handleQrCodeTypeSelect(action: ViewAsQrCodeAction.QrCodeTypeSelect) {
|
||||
// val currentState = state as? ViewAsQrCodeState.Content ?: return
|
||||
// val cipher = currentState.cipher
|
||||
//
|
||||
// // Generate default fields based on the selected QR code type and cipher data
|
||||
// val fields = when (action.qrCodeType) {
|
||||
// QrCodeType.Text -> mapOf("text" to cipher.name)
|
||||
// QrCodeType.Url -> {
|
||||
// val loginUri = cipher.login?.uris?.firstOrNull()?.uri ?: ""
|
||||
// mapOf("url" to loginUri)
|
||||
// }
|
||||
// QrCodeType.Email -> {
|
||||
// val email = cipher.login?.username.orEmpty()
|
||||
// mapOf(
|
||||
// "email" to email,
|
||||
// "subject" to "",
|
||||
// "body" to ""
|
||||
// )
|
||||
// }
|
||||
// QrCodeType.Phone -> {
|
||||
// val phone = when {
|
||||
// cipher.identity?.phone != null -> cipher.identity.phone
|
||||
// cipher.card?.cardholderName != null -> ""
|
||||
// else -> ""
|
||||
// }
|
||||
// mapOf("phone" to phone)
|
||||
// }
|
||||
// QrCodeType.SMS -> {
|
||||
// val phone = when {
|
||||
// cipher.identity?.phone != null -> cipher.identity.phone
|
||||
// else -> ""
|
||||
// }
|
||||
// mapOf(
|
||||
// "phone" to phone,
|
||||
// "message" to ""
|
||||
// )
|
||||
// }
|
||||
// QrCodeType.WiFi -> mapOf(
|
||||
// "ssid" to "",
|
||||
// "password" to "",
|
||||
// "type" to "WPA",
|
||||
// "hidden" to "false"
|
||||
// )
|
||||
// QrCodeType.Contact -> {
|
||||
// val name = when {
|
||||
// cipher.identity != null -> "${cipher.identity.firstName} ${cipher.identity.lastName}"
|
||||
// cipher.card?.cardholderName != null -> cipher.card.cardholderName
|
||||
// else -> cipher.name
|
||||
// }
|
||||
// val email = when {
|
||||
// cipher.identity?.email != null -> cipher.identity.email
|
||||
// cipher.login?.username != null -> cipher.login.username
|
||||
// else -> ""
|
||||
// }
|
||||
// val phone = when {
|
||||
// cipher.identity?.phone != null -> cipher.identity.phone
|
||||
// else -> ""
|
||||
// }
|
||||
// val organization = cipher.identity?.company ?: ""
|
||||
// val address = when {
|
||||
// cipher.identity != null -> "${cipher.identity.address1} ${cipher.identity.address2} ${cipher.identity.city} ${cipher.identity.state} ${cipher.identity.postalCode} ${cipher.identity.country}"
|
||||
// else -> ""
|
||||
// }
|
||||
//
|
||||
// mapOf(
|
||||
// "name" to name,
|
||||
// "phone" to phone,
|
||||
// "email" to email,
|
||||
// "organization" to organization,
|
||||
// "address" to address
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// val config = QrCodeConfig(action.qrCodeType, fields)
|
||||
// val qrCodeBitmap = QrCodeGenerator.generateQrCode(config)
|
||||
//
|
||||
// updateState {
|
||||
// (it as ViewAsQrCodeState.Content).copy(
|
||||
// selectedQrCodeType = action.qrCodeType,
|
||||
// fields = fields,
|
||||
// qrCodeBitmap = qrCodeBitmap
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
private fun handleFieldValueChange(action: ViewAsQrCodeAction.FieldValueChange) {
|
||||
// val currentState = state as? ViewAsQrCodeState.Content ?: return
|
||||
//
|
||||
// val updatedFields = currentState.fields.toMutableMap().apply {
|
||||
// put(action.fieldKey, action.value)
|
||||
// }
|
||||
//
|
||||
// val config = QrCodeConfig(currentState.selectedQrCodeType, updatedFields)
|
||||
// val qrCodeBitmap = QrCodeGenerator.generateQrCode(config)
|
||||
//
|
||||
// updateState {
|
||||
// (it as ViewAsQrCodeState.Content).copy(
|
||||
// fields = updatedFields,
|
||||
// qrCodeBitmap = qrCodeBitmap
|
||||
// )
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the state for viewing attachments.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ViewAsQrCodeState(
|
||||
val cipherId: String,
|
||||
val cipherType: VaultItemCipherType,
|
||||
val viewState: ViewState,
|
||||
val dialogState: DialogState?,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Represents the specific view states for the [ViewAsQrCodeScreen].
|
||||
*/
|
||||
sealed class ViewState : Parcelable {
|
||||
/**
|
||||
* Represents an error state for the [ViewAsQrCodeScreen].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Error(val message: Text) : ViewState()
|
||||
|
||||
/**
|
||||
* Loading state for the [ViewAsQrCodeScreen], signifying that the content is being
|
||||
* processed.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Loading : ViewState()
|
||||
|
||||
/**
|
||||
* Represents a loaded content state for the [ViewAsQrCodeScreen].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Content(
|
||||
|
||||
val title: String,
|
||||
// val qrCodeBitmap: Bitmap,
|
||||
// val selectedQrCodeType: QrCodeType,
|
||||
// val qrCodeTypes: ImmutableList<QrCodeType>,
|
||||
// val fields: Map<String, String>,
|
||||
// val cipher: com.x8bit.bitwarden.data.vault.datasource.model.Cipher
|
||||
// ) : ViewAsQrCodeState()
|
||||
//TODO add content?
|
||||
) : ViewState()
|
||||
}
|
||||
//TODO do we need dialogs?
|
||||
/**
|
||||
* Represents the current state of any dialogs on the screen.
|
||||
*/
|
||||
sealed class DialogState : Parcelable {
|
||||
/**
|
||||
* Represents a dismissible dialog with the given error [message].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Error(
|
||||
val title: Text?,
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
* Represents a loading dialog with the given [message].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Loading(
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Models events for the [ViewAsQrCodeScreen].
|
||||
*/
|
||||
sealed class ViewAsQrCodeEvent {
|
||||
/**
|
||||
* Navigate back.
|
||||
*/
|
||||
data object NavigateBack : ViewAsQrCodeEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a set of actions for [ViewAsQrCodeScreen].
|
||||
*/
|
||||
sealed class ViewAsQrCodeAction {
|
||||
/**
|
||||
* User clicked the back button.
|
||||
*/
|
||||
data object BackClick : ViewAsQrCodeAction()
|
||||
|
||||
//TODO deleteme
|
||||
/**
|
||||
* User selected a QR code type.
|
||||
*/
|
||||
data class QrCodeTypeSelect(val qrCodeType: QrCodeType) : ViewAsQrCodeAction()
|
||||
|
||||
/**
|
||||
* User changed a field value.
|
||||
*/
|
||||
data class FieldValueChange(val fieldKey: String, val value: String) : ViewAsQrCodeAction()
|
||||
|
||||
/**
|
||||
* Internal ViewModel actions.
|
||||
*/
|
||||
sealed class Internal : ViewAsQrCodeAction() {
|
||||
/**
|
||||
* The cipher data has been received.
|
||||
*/
|
||||
data class CipherReceive(
|
||||
val cipherDataState: DataState<CipherView?>,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.handlers
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.ViewAsQrCodeAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.ViewAsQrCodeViewModel
|
||||
|
||||
/**
|
||||
* A collection of handler functions for managing actions within the context of viewing as QR code.
|
||||
*/
|
||||
data class ViewAsQrCodeHandlers(
|
||||
val onBackClick: () -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
/**
|
||||
* Creates the [ViewAsQrCodeHandlers] using the [ViewAsQrCodeViewModel] to send desired
|
||||
* actions.
|
||||
*/
|
||||
fun create(viewModel: ViewAsQrCodeViewModel): ViewAsQrCodeHandlers =
|
||||
ViewAsQrCodeHandlers(
|
||||
onBackClick = { viewModel.trySendAction(ViewAsQrCodeAction.BackClick) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Represents the different types of QR codes that can be generated.
|
||||
*/
|
||||
sealed class QrCodeType(val displayName: Text) : Parcelable {
|
||||
|
||||
/**
|
||||
* Plain text QR code.
|
||||
*/
|
||||
@Parcelize
|
||||
data object PlainText : QrCodeType(R.string.text.asText())
|
||||
|
||||
/**
|
||||
* URL QR code.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Url : QrCodeType(R.string.url.asText())
|
||||
|
||||
/**
|
||||
* Email QR code.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Email : QrCodeType(R.string.email.asText())
|
||||
|
||||
/**
|
||||
* Phone number QR code.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Phone : QrCodeType(R.string.phone.asText())
|
||||
|
||||
/**
|
||||
* SMS QR code.
|
||||
*/
|
||||
@Parcelize
|
||||
data object SMS : QrCodeType(R.string.sms.asText())
|
||||
|
||||
/**
|
||||
* WiFi network QR code.
|
||||
*/
|
||||
@Parcelize
|
||||
data object WiFi : QrCodeType(R.string.wifi.asText())
|
||||
|
||||
/**
|
||||
* vCard contact QR code.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Contact : QrCodeType(R.string.contact.asText())
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* List of all available QR code types.
|
||||
*/
|
||||
val ALL = listOf(PlainText, Url, Email, Phone, SMS, WiFi, Contact)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the configuration options for a QR code.
|
||||
*/
|
||||
@Parcelize
|
||||
data class QrCodeConfig(
|
||||
val type: QrCodeType,
|
||||
val fields: Map<String, String> = emptyMap()
|
||||
) : Parcelable
|
||||
@ -0,0 +1,27 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.util
|
||||
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.ViewAsQrCodeState
|
||||
|
||||
/**
|
||||
* Converts the [CipherView] into a [ViewAsQrCodeState.ViewState.Content].
|
||||
*/
|
||||
fun CipherView.toViewState(): ViewAsQrCodeState.ViewState.Content =
|
||||
ViewAsQrCodeState.ViewState.Content(
|
||||
//TODO map to Content
|
||||
title = "From viewasqrcode.CipherViewExtensions.kt"
|
||||
|
||||
// originalCipher = this,
|
||||
// attachments = this
|
||||
// .attachments
|
||||
// .orEmpty()
|
||||
// .mapNotNull {
|
||||
// val id = it.id ?: return@mapNotNull null
|
||||
// AttachmentsState.AttachmentItem(
|
||||
// id = id,
|
||||
// title = it.fileName.orEmpty(),
|
||||
// displaySize = it.sizeName.orEmpty(),
|
||||
// )
|
||||
// },
|
||||
// newAttachment = null,
|
||||
)
|
||||
@ -0,0 +1,140 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.model.QrCodeType
|
||||
import java.net.URLEncoder
|
||||
|
||||
/**
|
||||
* Utility class for generating QR codes.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
object QrCodeGenerator {
|
||||
|
||||
private const val QR_CODE_SIZE = 512
|
||||
private const val UTF_8 = "UTF-8"
|
||||
//
|
||||
// /**
|
||||
// * Generate a QR code bitmap from the given configuration.
|
||||
// *
|
||||
// * @param config The QR code configuration.
|
||||
// * @return A bitmap containing the generated QR code.
|
||||
// */
|
||||
// fun generateQrCode(config: QrCodeConfig): Bitmap {
|
||||
// val content = formatQrCodeContent(config)
|
||||
// return generateQrCodeBitmap(content)
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Format the content for the QR code based on the configuration.
|
||||
// *
|
||||
// * @param config The QR code configuration.
|
||||
// * @return The formatted content string for the QR code.
|
||||
// */
|
||||
// private fun formatQrCodeContent(config: QrCodeConfig): String {
|
||||
// return when (config.type) {
|
||||
// QrCodeType.PlainText -> config.fields["text"] ?: ""
|
||||
// QrCodeType.Url -> config.fields["url"] ?: ""
|
||||
// QrCodeType.Email -> {
|
||||
// val email = config.fields["email"] ?: ""
|
||||
// val subject = config.fields["subject"] ?: ""
|
||||
// val body = config.fields["body"] ?: ""
|
||||
//
|
||||
// if (subject.isNotEmpty() || body.isNotEmpty()) {
|
||||
// val encodedSubject = URLEncoder.encode(subject, UTF_8)
|
||||
// val encodedBody = URLEncoder.encode(body, UTF_8)
|
||||
// "mailto:$email?subject=$encodedSubject&body=$encodedBody"
|
||||
// } else {
|
||||
// "mailto:$email"
|
||||
// }
|
||||
// }
|
||||
// QrCodeType.Phone -> "tel:${config.fields["phone"] ?: ""}"
|
||||
// QrCodeType.SMS -> {
|
||||
// val phone = config.fields["phone"] ?: ""
|
||||
// val message = config.fields["message"] ?: ""
|
||||
//
|
||||
// if (message.isNotEmpty()) {
|
||||
// val encodedMessage = URLEncoder.encode(message, UTF_8)
|
||||
// "smsto:$phone:$encodedMessage"
|
||||
// } else {
|
||||
// "smsto:$phone"
|
||||
// }
|
||||
// }
|
||||
// QrCodeType.WiFi -> {
|
||||
// val ssid = config.fields["ssid"] ?: ""
|
||||
// val password = config.fields["password"] ?: ""
|
||||
// val type = config.fields["type"] ?: "WPA"
|
||||
// val hidden = config.fields["hidden"] == "true"
|
||||
//
|
||||
// "WIFI:S:$ssid;T:$type;P:$password;H:$hidden;;"
|
||||
// }
|
||||
// QrCodeType.Contact -> {
|
||||
// val name = config.fields["name"] ?: ""
|
||||
// val phone = config.fields["phone"] ?: ""
|
||||
// val email = config.fields["email"] ?: ""
|
||||
// val organization = config.fields["organization"] ?: ""
|
||||
// val address = config.fields["address"] ?: ""
|
||||
//
|
||||
// buildString {
|
||||
// append("BEGIN:VCARD\n")
|
||||
// append("VERSION:3.0\n")
|
||||
// if (name.isNotEmpty()) append("N:$name\n")
|
||||
// if (name.isNotEmpty()) append("FN:$name\n")
|
||||
// if (organization.isNotEmpty()) append("ORG:$organization\n")
|
||||
// if (phone.isNotEmpty()) append("TEL:$phone\n")
|
||||
// if (email.isNotEmpty()) append("EMAIL:$email\n")
|
||||
// if (address.isNotEmpty()) append("ADR:;;$address\n")
|
||||
// append("END:VCARD")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Generate a QR code bitmap from the given content string.
|
||||
*
|
||||
* @param content The content to encode in the QR code.
|
||||
* @return A bitmap containing the generated QR code.
|
||||
*/
|
||||
private fun generateQrCodeBitmap(content: String): Bitmap {
|
||||
val hints = mapOf(
|
||||
EncodeHintType.CHARACTER_SET to UTF_8,
|
||||
EncodeHintType.MARGIN to 1
|
||||
)
|
||||
|
||||
val bitMatrix = MultiFormatWriter().encode(
|
||||
content,
|
||||
BarcodeFormat.QR_CODE,
|
||||
QR_CODE_SIZE,
|
||||
QR_CODE_SIZE,
|
||||
hints
|
||||
)
|
||||
|
||||
return createBitmapFromBitMatrix(bitMatrix)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a bitmap from a ZXing BitMatrix.
|
||||
*
|
||||
* @param bitMatrix The BitMatrix to convert.
|
||||
* @return A bitmap representation of the BitMatrix.
|
||||
*/
|
||||
private fun createBitmapFromBitMatrix(bitMatrix: BitMatrix): Bitmap {
|
||||
val width = bitMatrix.width
|
||||
val height = bitMatrix.height
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
bitmap.setPixel(x, y, if (bitMatrix[x, y]) Color.BLACK else Color.WHITE)
|
||||
}
|
||||
}
|
||||
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
@ -1229,4 +1229,18 @@ Do you want to switch to this account?</string>
|
||||
<string name="add_field">Add field</string>
|
||||
<string name="x_ellipses">%s...</string>
|
||||
<string name="share_error_details">Share error details</string>
|
||||
|
||||
<string name="view_as_qr_code">View as QR code</string>
|
||||
<string name="qr_code">QR code</string>
|
||||
<string name="qr_code_type">QR code type</string>
|
||||
<string name="url">URL</string>
|
||||
<string name="subject">Subject</string>
|
||||
<string name="body">Body</string>
|
||||
<string name="message">Message</string>
|
||||
<string name="ssid">SSID</string>
|
||||
<string name="encryption_type">Encryption Type</string>
|
||||
<string name="hidden">Hidden</string>
|
||||
<string name="sms">SMS</string>
|
||||
<string name="wifi">WiFi</string>
|
||||
<string name="contact">Contact</string>
|
||||
</resources>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user