Generate a QR code to export all account details as JSON (#735)

* Generate QR code to export account details as JSON

* create a popup window screen for displaying qr code

* QR code save to gallery feature

* Move export via QR code button to the bottom bar

* Fix PR checks failing

---------

Co-authored-by: Divyam Pahuja <u7467739@anu.edu.au>
This commit is contained in:
Divyam
2023-10-18 16:28:55 +11:00
committed by GitHub
parent a0bcb20664
commit 5ea20b73e3
4 changed files with 139 additions and 4 deletions

View File

@@ -1,14 +1,32 @@
package com.yogeshpaliyal.keypass.ui.detail
import android.graphics.Bitmap
import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import com.yogeshpaliyal.common.constants.ScannerType
import com.yogeshpaliyal.common.data.AccountModel
@@ -18,6 +36,12 @@ import com.yogeshpaliyal.keypass.ui.detail.components.Fields
import com.yogeshpaliyal.keypass.ui.redux.actions.CopyToClipboard
import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
import org.reduxkotlin.compose.rememberDispatcher
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/*
* @author Yogesh Paliyal
@@ -32,6 +56,8 @@ fun AccountDetailPage(
viewModel: DetailViewModel = hiltViewModel()
) {
val dispatchAction = rememberDispatcher()
val generatedQrCodeBitmap = remember { mutableStateOf<Bitmap?>(null) }
val showDialog = remember { mutableStateOf(false) }
// task value state
val (accountModel, setAccountModel) = remember {
@@ -68,12 +94,10 @@ fun AccountDetailPage(
if (newAccountModel.username.isNullOrEmpty()) {
newAccountModel = newAccountModel.copy(username = totp.issuer)
}
setAccountModel(newAccountModel)
}
}
}
Scaffold(
bottomBar = {
BottomBar(
@@ -81,6 +105,12 @@ fun AccountDetailPage(
backPressed = goBack,
onDeleteAccount = {
viewModel.deleteAccount(accountModel, goBack)
},
generateQrCodeClicked = {
showDialog.value = true
val qrCodeBitmap = viewModel.generateQrCode(accountModel)
generatedQrCodeBitmap.value = qrCodeBitmap
}
) {
viewModel.insertOrUpdate(accountModel, goBack)
@@ -99,6 +129,75 @@ fun AccountDetailPage(
) {
launcher.launch(it)
}
// Display the generated QR code bitmap in a popup
if (showDialog.value) {
val download = remember { mutableStateOf(false) }
Dialog(onDismissRequest = { showDialog.value = false }) {
AlertDialog(
onDismissRequest = { showDialog.value = false },
title = {
Text("QR Code")
},
text = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp)
) {
generatedQrCodeBitmap.value?.asImageBitmap()?.let {
Image(
bitmap = it,
contentDescription = null,
modifier = Modifier.size(150.dp)
)
}
}
},
dismissButton = {
Button(onClick = {
download.value = true
}) {
Text("Download QR Code")
}
},
confirmButton = {
Button(onClick = { showDialog.value = false }) {
Text("Close")
}
}
)
}
if (download.value) {
generatedQrCodeBitmap.value?.let { saveQRCodeImage(imageBitmap = it.asImageBitmap(), displayName = "QRCode") }
}
}
}
}
}
@Composable
fun saveQRCodeImage(imageBitmap: ImageBitmap, displayName: String) {
val context = LocalContext.current
val currentTimeMillis = System.currentTimeMillis()
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date(currentTimeMillis))
val fileName = "$displayName-$timeStamp.png"
val resolver = context.contentResolver
val bitmap: Bitmap = imageBitmap.asAndroidBitmap()
val imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
val image = File(imagesDir, fileName)
try {
val fos: OutputStream = FileOutputStream(image)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
fos.flush()
fos.close()
MediaStore.Images.Media.insertImage(resolver, image.absolutePath, fileName, null)
Toast.makeText(context, "QR code saved to gallery", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(context, "Error occurred while downloading QR code", Toast.LENGTH_SHORT).show()
}
}

View File

@@ -1,16 +1,24 @@
package com.yogeshpaliyal.keypass.ui.detail
import android.app.Application
import android.graphics.Bitmap
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.google.gson.Gson
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.MultiFormatWriter
import com.google.zxing.WriterException
import com.google.zxing.common.BitMatrix
import com.yogeshpaliyal.common.data.AccountModel
import com.yogeshpaliyal.common.worker.executeAutoBackup
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
import javax.inject.Inject
/*
@@ -45,7 +53,6 @@ class DetailViewModel @Inject constructor(
}
}
}
fun insertOrUpdate(accountModel: AccountModel, onExecCompleted: () -> Unit) {
viewModelScope.launch {
accountModel.let {
@@ -57,10 +64,26 @@ class DetailViewModel @Inject constructor(
onExecCompleted()
}
}
private fun autoBackup() {
viewModelScope.launch {
app.executeAutoBackup()
}
}
fun generateQrCode(accountModel: AccountModel): Bitmap? {
val accountJson = Gson().toJson(accountModel)
println("JSON String:$accountJson")
val hints = Hashtable<EncodeHintType, String>()
hints[EncodeHintType.CHARACTER_SET] = "UTF-8"
val multiFormatWriter = MultiFormatWriter()
try {
val bitMatrix: BitMatrix = multiFormatWriter.encode(accountJson, BarcodeFormat.QR_CODE, 200, 200, hints)
val barcodeEncoder = com.journeyapps.barcodescanner.BarcodeEncoder()
return barcodeEncoder.createBitmap(bitMatrix)
} catch (e: WriterException) {
e.printStackTrace()
}
return null
}
}

View File

@@ -1,6 +1,7 @@
package com.yogeshpaliyal.keypass.ui.detail.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.rounded.ArrowBackIosNew
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Done
@@ -22,6 +23,7 @@ fun BottomBar(
accountModel: AccountModel,
backPressed: () -> Unit,
onDeleteAccount: () -> Unit,
generateQrCodeClicked: () -> Unit,
onSaveClicked: () -> Unit
) {
val openDialog = remember { mutableStateOf(false) }
@@ -47,6 +49,16 @@ fun BottomBar(
tint = MaterialTheme.colorScheme.onSurface
)
}
IconButton(
modifier = Modifier.testTag("action_export_qr"),
onClick = { generateQrCodeClicked() }
) {
Icon(
painter = rememberVectorPainter(image = Icons.Default.QrCode),
contentDescription = "Export as QR Code",
tint = MaterialTheme.colorScheme.onSurface
)
}
}
},
floatingActionButton = {

View File

@@ -97,5 +97,6 @@
<string name="secret_key">Secret Key</string>
<string name="keypass_backup">KeyPass Backup</string>
<string name="google_backup">Google Backup</string>
<string name="generate_qr_code">Export</string>
</resources>