Draft - Set cipher values and QR Code update

This commit is contained in:
Álison Fernandes 2025-03-24 22:12:33 +00:00
parent 605e0ef023
commit 6ec84fbd46
No known key found for this signature in database
GPG Key ID: B8CE98903DFC87BC
5 changed files with 207 additions and 61 deletions

View File

@ -8,6 +8,7 @@ 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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@ -32,6 +33,8 @@ import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
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.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
@ -96,10 +99,25 @@ fun ViewAsQrCodeScreen(
)
}
Spacer(modifier = Modifier.height(12.dp))
//TODO debug - remove this
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextField(
label = "Debug - QRCode Content",
value = state.qrCodeContent,
onValueChange = { },
readOnly = true,
singleLine = false,
textFieldTestTag = "LoginUsernameEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("QRCodeType")
.standardHorizontalMargin()
.fillMaxWidth(),
)
// QR Code type selector
val resources = LocalContext.current.resources
Spacer(modifier = Modifier.height(12.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.qr_code_type),
options = viewState.qrCodeTypes.map { it.displayName() }.toImmutableList(),
@ -119,7 +137,15 @@ fun ViewAsQrCodeScreen(
)
//QR Code Type dropdowns
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.data_to_share),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
viewState.qrCodeTypeFields.forEachIndexed { i, field ->
val cipherFieldsTextList =
viewState.cipherFields.map { it() }.toImmutableList()

View File

@ -27,6 +27,7 @@ import kotlinx.parcelize.Parcelize
import javax.inject.Inject
private const val KEY_STATE = "state"
private const val DEFAULT_QR_CODE_DATA = "https://bitwarden.com"
/**
* ViewModel responsible for handling user interactions in the attachments screen.
@ -45,13 +46,13 @@ class ViewAsQrCodeViewModel @Inject constructor(
ViewAsQrCodeState(
cipherId = args.vaultItemId,
cipherType = args.vaultItemCipherType,
qrCodeBitmap = QrCodeGenerator.generateQrCodeBitmap("↑, ↑, ↓, ↓, ←, →, ←, →, B, A,↑, ↑, ↓, ↓, ←, →, ←, →, B, A,↑, ↑, ↓, ↓, ←, →, ←, →, B, A,↑, ↑, ↓, ↓, ←, →, ←, →, B, A,↑, ↑, ↓, ↓, ←, →, ←, →, B, A,"),
qrCodeBitmap = QrCodeGenerator.generateQrCodeBitmap(DEFAULT_QR_CODE_DATA),
selectedQrCodeType = selectedQrCodeType,
qrCodeTypes = qrCodeTypes,
qrCodeTypeFields = selectedQrCodeType.fields,
cipherFields = emptyList(),
cipher = null,
qrCodeContent = DEFAULT_QR_CODE_DATA,
// viewState = ViewAsQrCodeState.ViewState.Loading,
// dialogState = null,
)
@ -61,11 +62,11 @@ class ViewAsQrCodeViewModel @Inject constructor(
init {
//TODO get args.vaultItemCipherType and auto-map
mutableStateFlow.update {
it.copy(
cipherFields = cipherFieldsFor(it.cipherType, null),
)
}
// mutableStateFlow.update {
// it.copy(
// cipherFields = cipherFieldsFor(it.cipherType, null),
// )
// }
vaultRepository
.getVaultItemStateFlow(args.vaultItemId)
.map { ViewAsQrCodeAction.Internal.CipherReceive(it) }
@ -118,61 +119,32 @@ class ViewAsQrCodeViewModel @Inject constructor(
is DataState.Loading -> {}
is DataState.NoNetwork -> {}
is DataState.Pending -> {}
// 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.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 updatedFields = autoMapFields(
action.qrCodeType.fields,
state.cipherType,
state.cipher
)
setCipherValues(state.cipher, state.selectedQrCodeType, updatedFields)
val updatedQrCodeContent = if (validateRequiredFields(updatedFields))
QrCodeGenerator.createContentFor(state.selectedQrCodeType, updatedFields)
else
DEFAULT_QR_CODE_DATA
val updatedQrCodeBitmap = QrCodeGenerator.generateQrCodeBitmap(updatedQrCodeContent)
mutableStateFlow.update {
it.copy(
selectedQrCodeType = action.qrCodeType,
qrCodeTypeFields = autoMapFields(
action.qrCodeType.fields,
state.cipherType,
state.cipher
)
qrCodeTypeFields = updatedFields,
qrCodeBitmap = updatedQrCodeBitmap,
qrCodeContent = updatedQrCodeContent,
)
}
}
@ -195,7 +167,7 @@ class ViewAsQrCodeViewModel @Inject constructor(
val selectedText = value.asText()
//TODO should we transition qrCodeTypeFields to a map again*2 and update using key?
val updatedFields = state.qrCodeTypeFields.map { currentField ->
var updatedFields = state.qrCodeTypeFields.map { currentField ->
if (currentField.key == field.key) {
currentField.copy(value = selectedText)
} else {
@ -203,11 +175,56 @@ class ViewAsQrCodeViewModel @Inject constructor(
}
}
updatedFields = setCipherValues(state.cipher, state.selectedQrCodeType, updatedFields)
val updatedQrCodeContent = if (validateRequiredFields(updatedFields))
QrCodeGenerator.createContentFor(state.selectedQrCodeType, updatedFields)
else
DEFAULT_QR_CODE_DATA
val updatedQrCodeBitmap = QrCodeGenerator.generateQrCodeBitmap(updatedQrCodeContent)
mutableStateFlow.update {
it.copy(qrCodeTypeFields = updatedFields)
it.copy(
qrCodeTypeFields = updatedFields,
qrCodeBitmap = updatedQrCodeBitmap,
qrCodeContent = updatedQrCodeContent
)
}
}
private fun setCipherValues(
cipher: CipherView?,
qrCodeType: QrCodeType,
qrCodeTypeFields: List<QrCodeTypeField>,
): List<QrCodeTypeField> {
if (cipher == null) {
return qrCodeTypeFields
}
//TODO figure a way to reuse autoMapField code, by returning (field Text + Cipher value) perhaps
return qrCodeTypeFields.map { field ->
field.copy(cipherValue = getCipherValueForField(cipher, field))
}
}
private fun getCipherValueForField(cipher: CipherView, qrCodeField: QrCodeTypeField): String {
//TODO refactor - currently failing because asText() creates a different object
return when (qrCodeField.value) {
R.string.name.asText() -> cipher.name
R.string.username.asText() -> cipher.login?.username ?: ""
R.string.password.asText() -> cipher.login?.username ?: ""
R.string.notes.asText() -> cipher.notes ?: ""
"Custom: SSID".asText() -> cipher.fields?.find { it.name == "Custom: SSID" }?.value
?: ""
else -> "TODO()" //TODO
}
}
private fun validateRequiredFields(qrCodeTypeFields: List<QrCodeTypeField>): Boolean {
return true // TODO
}
private fun autoMapField(
qrCodeTypeField: QrCodeTypeField,
cipherType: VaultItemCipherType,
@ -304,7 +321,8 @@ data class ViewAsQrCodeState(
@IgnoredOnParcel
val cipherFields: List<Text> = emptyList(),
@IgnoredOnParcel
val cipher: CipherView? = null, //TODO do we need to use null?
val cipher: CipherView? = null,
val qrCodeContent: String = "",
) : Parcelable
/**

View File

@ -104,5 +104,6 @@ data class QrCodeTypeField(
val key: String,
val displayName: Text,
val isRequired: Boolean = false,
var value: Text = "".asText(),
val value: Text = "".asText(),
val cipherValue: String = "",
) : Parcelable

View File

@ -10,6 +10,8 @@ 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 com.x8bit.bitwarden.ui.vault.feature.viewasqrcode.model.QrCodeTypeField
/**
* Utility class for generating QR codes.
@ -19,6 +21,104 @@ object QrCodeGenerator {
private const val QR_CODE_SIZE = 512 + 256
private const val UTF_8 = "UTF-8"
fun createContentFor(qrCodeType: QrCodeType, qrCodeFields: List<QrCodeTypeField>): String {
return when (qrCodeType) {
QrCodeType.WIFI -> createContentForWifi(qrCodeFields)
QrCodeType.URL -> createContentForText(qrCodeFields)
QrCodeType.PLAIN_TEXT -> createContentForText(qrCodeFields)
QrCodeType.EMAIL -> createContentForEmail(qrCodeFields)
QrCodeType.PHONE -> createContentForPhone(qrCodeFields)
QrCodeType.CONTACT_VCARD -> createContentForContactVcard(qrCodeFields)
QrCodeType.CONTACT_MECARD -> createContentForContactMecard(qrCodeFields)
}
}
fun generateQrCode(qrCodeType: QrCodeType, qrCodeFields: List<QrCodeTypeField>): Bitmap {
val content = createContentFor(qrCodeType, qrCodeFields)
return generateQrCodeBitmap(content)
}
fun createContentForContactMecard(qrCodeFields: List<QrCodeTypeField>): String {
val firstName = "firstName"
val lastName = "lastName"
val phone = "phone"
val email = "email"
val address = "address"
val addName = lastName.isNotEmpty() || firstName.isNotEmpty()
return buildString {
append("MECARD:")
if (addName) {
append("N:")
if (lastName.isNotEmpty()) append(lastName)
if (firstName.isNotEmpty()) append(",$firstName")
append(";")
}
if (phone.isNotEmpty()) append("TEL:$phone;")
if (email.isNotEmpty()) append("EMAIL:$email;")
if (address.isNotEmpty()) append("ADR:$address;")
append(";")
}
}
fun createContentForContactVcard(qrCodeFields: List<QrCodeTypeField>): String {
val name = "name"
val phone = "phone"
val email = "email"
val organization = "organization"
val address = "address"
return 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")
}
}
fun createContentForWifi(qrCodeFields: List<QrCodeTypeField>): String {
var ssid = String()
var password = String()
var additionalOptions = String()
qrCodeFields.forEach {
when (it.key) {
"ssid" -> ssid = it.cipherValue
"password" -> password = it.cipherValue
"additionalOptions" -> additionalOptions = it.cipherValue
}
}
return buildString {
append("WIFI:")
if (password.isNotEmpty() && !additionalOptions.contains("T:")) append("T:WPA;")
append("ssid:$ssid;")
if (password.isNotEmpty()) append("P:$password;")
if (additionalOptions.isNotEmpty()) append(additionalOptions)
append(";")
}
}
fun createContentForText(qrCodeFields: List<QrCodeTypeField>): String {
return qrCodeFields.firstOrNull()?.cipherValue ?: ""
}
fun createContentForEmail(qrCodeFields: List<QrCodeTypeField>): String {
val value = qrCodeFields.firstOrNull()?.cipherValue ?: ""
return "email:$value"
}
fun createContentForPhone(qrCodeFields: List<QrCodeTypeField>): String {
val value = qrCodeFields.firstOrNull()?.cipherValue ?: ""
return "phone:$value"
}
//
// /**
// * Generate a QR code bitmap from the given configuration.
@ -140,7 +240,7 @@ object QrCodeGenerator {
val contentColor = "#165DDC".toColorInt() // bitwarden blue
val finderPatternColor = "#030E65".toColorInt() // dark blue
val backgroundColor = Color.WHITE
for (x in 0 until width) {
for (y in 0 until height) {
val bit = bitMatrix[x, y]

View File

@ -1231,6 +1231,7 @@ Do you want to switch to this account?</string>
<string name="share_error_details">Share error details</string>
<string name="view_as_qr_code">View as QR code</string>
<string name="data_to_share">Data to share</string>
<string name="qr_code">QR code</string>
<string name="qr_code_type">QR code type</string>
<string name="url">URL</string>