From 82a95ae91d49b1ae7c6f86a19495ffb66ae36d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hrib?= <103599310+tomashrib@users.noreply.github.com> Date: Tue, 17 Jan 2023 21:25:50 +0100 Subject: [PATCH] Scan btc address from QR code (#27) * FEAT: Scan address from QR code * BUG: BTC address prefix "bitcoin:" checking * BUG: Fixed wrong check --- .idea/misc.xml | 2 +- app/build.gradle | 6 + app/src/main/AndroidManifest.xml | 2 + .../tomashrib/zephyruswallet/tools/QRCodes.kt | 83 +++++++++ .../xyz/tomashrib/zephyruswallet/ui/Screen.kt | 2 + .../zephyruswallet/ui/wallet/QRScanScreen.kt | 158 ++++++++++++++++++ .../zephyruswallet/ui/wallet/SendScreen.kt | 103 +++++++++--- .../ui/wallet/WalletNavigation.kt | 3 + 8 files changed, 331 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/xyz/tomashrib/zephyruswallet/tools/QRCodes.kt create mode 100644 app/src/main/java/xyz/tomashrib/zephyruswallet/ui/wallet/QRScanScreen.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index 5c9f89f..54d5acd 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + diff --git a/app/build.gradle b/app/build.gradle index 0ba10c8..02fcd1e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -103,4 +103,10 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2" + + // QR codes + implementation("com.google.zxing:core:3.4.1") + implementation("androidx.camera:camera-camera2:1.1.0-rc01") + implementation("androidx.camera:camera-lifecycle:1.1.0-rc01") + implementation("androidx.camera:camera-view:1.1.0-rc01") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dff0afd..d6da957 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,8 @@ xmlns:tools="http://schemas.android.com/tools"> + + Unit +) : ImageAnalysis.Analyzer { + + companion object { + private val SUPPORTED_IMAGE_FORMATS = listOf(ImageFormat.YUV_420_888, ImageFormat.YUV_422_888, ImageFormat.YUV_444_888) + } + + override fun analyze(image: ImageProxy) { + if (image.format in SUPPORTED_IMAGE_FORMATS) { + val bytes = image.planes.first().buffer.toByteArray() + val source = PlanarYUVLuminanceSource( + bytes, + image.width, + image.height, + 0, + 0, + image.width, + image.height, + false + ) + val binaryBitmap = BinaryBitmap(HybridBinarizer(source)) + try { + val result = MultiFormatReader().apply { + setHints( + mapOf( + DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE) + ) + ) + }.decode(binaryBitmap) + Log.i(tag, "QR code scanned is ${result.text}") + onQrCodeScanned(result.text) + } catch (e: Exception) { + e.printStackTrace() + } finally { + image.close() + } + } + } + + private fun ByteBuffer.toByteArray(): ByteArray { + rewind() + return ByteArray(remaining()).also { get(it) } + } +} + +fun addressToQR(address: String): ImageBitmap? { + Log.i(tag, "We are generating the QR code for address $address") + try { + val qrCodeWriter = QRCodeWriter() + val bitMatrix: BitMatrix = qrCodeWriter.encode(address, BarcodeFormat.QR_CODE, 1000, 1000) + val bitMap = createBitmap(1000, 1000) + for (x in 0 until 1000) { + for (y in 0 until 1000) { + // uses night1 and md_theme_dark_onPrimary for colors + bitMap.setPixel(x, y, if (bitMatrix[x, y]) 0xff000000.toInt() else 0xfff3f4ff.toInt()) + // bitMap.setPixel(x, y, if (bitMatrix[x, y]) 0xFF3c3836.toInt() else 0xFFebdbb2.toInt()) + } + } + // Log.i(TAG, "QR is ${bitMap.asImageBitmap()}") + return bitMap.asImageBitmap() + } catch (e: Throwable) { + Log.i(tag, "Error with QRCode generation, $e") + } + return null +} \ No newline at end of file diff --git a/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/Screen.kt b/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/Screen.kt index e734ac5..78720e5 100644 --- a/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/Screen.kt +++ b/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/Screen.kt @@ -16,4 +16,6 @@ sealed class Screen(val route: String) { object SendScreen : Screen("send_screen") object ReceiveScreen : Screen("receive_screen") object TransactionsScreen : Screen("transactions_screen") + + object QRScanScreen: Screen("qr_scan_screen") } diff --git a/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/wallet/QRScanScreen.kt b/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/wallet/QRScanScreen.kt new file mode 100644 index 0000000..dd8a61c --- /dev/null +++ b/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/wallet/QRScanScreen.kt @@ -0,0 +1,158 @@ +package xyz.tomashrib.zephyruswallet.ui.wallet + +import android.Manifest +import android.content.pm.PackageManager +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.navigation.NavHostController +import xyz.tomashrib.zephyruswallet.tools.QRCodeAnalyzer +import xyz.tomashrib.zephyruswallet.ui.theme.ZephyrusColors +import xyz.tomashrib.zephyruswallet.ui.theme.sourceSans + +@Composable +internal fun QRScanScreen(navController: NavHostController) { + + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + val snackbarHostState = remember { SnackbarHostState() } + val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) } + + var hasCameraPermission by remember { + mutableStateOf( + ContextCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + ) + } + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { granted -> + hasCameraPermission = granted + } + ) + + LaunchedEffect(key1 = true) { + launcher.launch(Manifest.permission.CAMERA) + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + backgroundColor = ZephyrusColors.bgColorBlack, + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { + ConstraintLayout( + modifier = Modifier.fillMaxSize(), + ) { + val (camera, cancelButton) = createRefs() + + Box( + modifier = Modifier + .background(ZephyrusColors.bgColorBlack) + .constrainAs(camera) { + top.linkTo(parent.top) + absoluteLeft.linkTo(parent.absoluteLeft) + absoluteRight.linkTo(parent.absoluteRight) + bottom.linkTo(parent.bottom) + } + ) { + Column { + if (hasCameraPermission) { + AndroidView( + factory = { context -> + val previewView = PreviewView(context) + val preview = Preview.Builder().build() + val selector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build() + preview.setSurfaceProvider(previewView.surfaceProvider) + val imageAnalysis = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + imageAnalysis.setAnalyzer( + ContextCompat.getMainExecutor(context), + QRCodeAnalyzer { result -> + result?.let { + navController.previousBackStackEntry + ?.savedStateHandle + ?.set("BTC_Address", it) + navController.popBackStack() + } + } + ) + + try { + cameraProviderFuture.get().bindToLifecycle( + lifecycleOwner, + selector, + preview, + imageAnalysis + ) + } catch (e: Exception) { + e.printStackTrace() + } + + return@AndroidView previewView + }, + modifier = Modifier.weight(weight = 1f) + ) + } + } + } + + Button( + onClick = { + navController.popBackStack() + }, + colors = ButtonDefaults.buttonColors(ZephyrusColors.lightPurplePrimary), + shape = RoundedCornerShape(20.dp), +// border = BorderStroke(3.dp, ZephyrusColors.fontColorWhite), + modifier = Modifier + .constrainAs(cancelButton) { + start.linkTo(parent.start, margin = 16.dp) + end.linkTo(parent.end, margin = 16.dp) + bottom.linkTo(parent.bottom, margin = 16.dp) + } + .padding(top = 4.dp, start = 4.dp, end = 4.dp, bottom = 4.dp) + .height(70.dp) + .width(200.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text( + text = "Cancel", + color = ZephyrusColors.darkerPurpleOnPrimary, + fontSize = 18.sp, + fontFamily = sourceSans + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/wallet/SendScreen.kt b/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/wallet/SendScreen.kt index 32e426e..9193d0c 100644 --- a/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/wallet/SendScreen.kt +++ b/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/wallet/SendScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.material.OutlinedTextField import androidx.compose.material.TextFieldDefaults import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow @@ -43,6 +44,23 @@ internal fun SendScreen(navController: NavController, context: Context){ val amount: MutableState = remember { mutableStateOf("") } val feeRate: MutableState = remember { mutableStateOf("") } + val qrCodeScanner = + navController.currentBackStackEntry?.savedStateHandle?.getLiveData("BTC_Address") + ?.observeAsState() + qrCodeScanner?.value.let { + if (it != null){ + if(it.substring(0,8) == "bitcoin:"){ + recipientAddress.value = it.substring(8) +// Log.i("qrcode", "${it.substring(0,8)} -> ${it.substring(8)}") + } else { + recipientAddress.value = it + } + } +// Log.i("qrcode", "naskenovana: ${recipientAddress.value}") + + navController.currentBackStackEntry?.savedStateHandle?.remove("BTC_Address") + } + ConstraintLayout( modifier = Modifier .fillMaxSize() @@ -74,43 +92,70 @@ internal fun SendScreen(navController: NavController, context: Context){ bottom.linkTo(broadcastButton.top) start.linkTo(parent.start) end.linkTo(parent.end) - height = Dimension.fillToConstraints +// height = Dimension.fillToConstraints } ){ - //paste address button - Text( - text = stringResource(R.string.paste_address), - fontSize = 15.sp, - fontFamily = sourceSans, - color = ZephyrusColors.lightPurplePrimary, + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier - .align(Alignment.End) + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + Text( + text = "Scan QR code", + fontSize = 15.sp, + fontFamily = sourceSans, + color = ZephyrusColors.lightPurplePrimary, + modifier = Modifier - //upon click, the address from clipboard is inserted into recipientAddress input field - .clickable { - try { + //upon click, the address from clipboard is inserted into recipientAddress input field + .clickable { - //checks if input from clipboard passed safety checks from function - if (pasteFromClipboard(context) != "Wrong format") { - recipientAddress.value = pasteFromClipboard(context) - } else { + navController.navigate(Screen.QRScanScreen.route) { + launchSingleTop = true + } + } + ) - //notifies user that address from clipboard is not of correct format + //paste address button + Text( + text = stringResource(R.string.paste_address), + fontSize = 15.sp, + fontFamily = sourceSans, + color = ZephyrusColors.lightPurplePrimary, + modifier = Modifier + + //upon click, the address from clipboard is inserted into recipientAddress input field + .clickable { + try { + + //checks if input from clipboard passed safety checks from function + if (pasteFromClipboard(context) != "Wrong format") { + recipientAddress.value = pasteFromClipboard(context) + } else { + + //notifies user that address from clipboard is not of correct format + Toast + .makeText(context, "Wrong address format", Toast.LENGTH_SHORT) + .show() + } + } catch (e: Exception) { + + //notifies user that they have empty clipboard Toast - .makeText(context, "Wrong address format", Toast.LENGTH_SHORT) + .makeText(context, "Empty clipboard", Toast.LENGTH_SHORT) .show() + Log.i(TAG, "Error while pasting from clipboard: $e") } - } catch (e: Exception) { - - //notifies user that they have empty clipboard - Toast - .makeText(context, "Empty clipboard", Toast.LENGTH_SHORT) - .show() - Log.i(TAG, "Error while pasting from clipboard: $e") } - } - ) + ) + } + + + + TransactionAddressInput(recipientAddress) TransactionAmountInput(amount) @@ -124,6 +169,7 @@ internal fun SendScreen(navController: NavController, context: Context){ color = ZephyrusColors.lightPurplePrimary, modifier = Modifier .align(Alignment.Start) + .padding(start = 20.dp) //when clicked, it puts wallet balance value into amount input field .clickable { @@ -143,6 +189,7 @@ internal fun SendScreen(navController: NavController, context: Context){ color = ZephyrusColors.lightPurplePrimary, modifier = Modifier .align(Alignment.End) + .padding(end = 20.dp) //when clicked it clears all input fields .clickable { @@ -218,7 +265,9 @@ private fun TransactionAddressInput(recipientAddress: MutableState){ .padding(bottom = 10.dp) .fillMaxWidth(0.9f), value = recipientAddress.value, - onValueChange = { recipientAddress.value = it }, + onValueChange = { + recipientAddress.value = it + }, label = { Text( text = stringResource(R.string.send_address), diff --git a/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/wallet/WalletNavigation.kt b/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/wallet/WalletNavigation.kt index 89f6459..3881a36 100644 --- a/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/wallet/WalletNavigation.kt +++ b/app/src/main/java/xyz/tomashrib/zephyruswallet/ui/wallet/WalletNavigation.kt @@ -34,5 +34,8 @@ fun WalletNavigation() { // composable( // route = Screen.TransactionsScreen.route, // ) { TransactionsScreen(navController) } + composable( + route = Screen.QRScanScreen.route, + ) { QRScanScreen(navController = navController)} } } \ No newline at end of file