diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/AccountDetailPage.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/AccountDetailPage.kt index 47916ba8..eddce3e8 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/AccountDetailPage.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/AccountDetailPage.kt @@ -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(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() + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailViewModel.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailViewModel.kt index c0d7c912..b70b1cb5 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailViewModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailViewModel.kt @@ -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() + 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 + } } diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/components/BottomBar.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/components/BottomBar.kt index 2c795401..0dcda2d3 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/components/BottomBar.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/components/BottomBar.kt @@ -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 = { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f76a6abf..af01a6ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -97,5 +97,6 @@ Secret Key KeyPass Backup Google Backup + Export