Skip to content

Commit

Permalink
Scan btc address from QR code (#27)
Browse files Browse the repository at this point in the history
* FEAT: Scan address from QR code

* BUG: BTC address prefix "bitcoin:" checking

* BUG: Fixed wrong check
  • Loading branch information
tohrxyz authored Jan 17, 2023
1 parent 7605969 commit 82a95ae
Show file tree
Hide file tree
Showing 8 changed files with 331 additions and 28 deletions.
2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:name=".ZephyrusWalletApplication"
android:allowBackup="true"
Expand Down
83 changes: 83 additions & 0 deletions app/src/main/java/xyz/tomashrib/zephyruswallet/tools/QRCodes.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package xyz.tomashrib.zephyruswallet.tools

import android.graphics.ImageFormat
import android.util.Log
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.core.graphics.createBitmap
import com.google.zxing.*
import com.google.zxing.common.BitMatrix
import com.google.zxing.common.HybridBinarizer
import com.google.zxing.qrcode.QRCodeWriter
import java.nio.ByteBuffer

private const val tag = "QrCodes"

class QRCodeAnalyzer(
private val onQrCodeScanned: (result: String?) -> 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
}
2 changes: 2 additions & 0 deletions app/src/main/java/xyz/tomashrib/zephyruswallet/ui/Screen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
}
}
}
Loading

0 comments on commit 82a95ae

Please sign in to comment.