mirror of
https://github.com/yogeshpaliyal/KeyPass.git
synced 2026-01-06 09:09:44 -06:00
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:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user