mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 08:35:05 -06:00
Draft - Set cipher values and QR Code update
This commit is contained in:
parent
605e0ef023
commit
6ec84fbd46
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
/**
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user