Skip to content

Commit

Permalink
androidApp: Initial support for tickets [2/3]
Browse files Browse the repository at this point in the history
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
  • Loading branch information
theimpulson committed Nov 26, 2024
1 parent e61d787 commit 09cd734
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 5 deletions.
6 changes: 5 additions & 1 deletion androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ dependencies {
implementation(libs.google.hilt.android.core)

// ZXing/Camera (QR)
implementation(libs.androidx.camera)
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)

implementation(libs.zxing.core)
implementation(libs.zxing.cpp)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@

package app.opass.ccip.android.ui.screens.ticket

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
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.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
Expand All @@ -22,7 +31,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
Expand All @@ -33,6 +41,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
Expand All @@ -56,8 +65,12 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.content.ContextCompat
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import app.opass.ccip.android.R
Expand All @@ -73,6 +86,9 @@ import coil.compose.SubcomposeAsyncImage
import coil.request.CachePolicy
import coil.request.ImageRequest
import kotlinx.coroutines.android.awaitFrame
import zxingcpp.BarcodeReader
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

@Composable
fun Screen.Ticket.TicketScreen(
Expand Down Expand Up @@ -162,9 +178,16 @@ private fun RequestTicket(
val eventConfig by viewModel.eventConfig.collectAsStateWithLifecycle()
val isVerifying by viewModel.isVerifying.collectAsStateWithLifecycle()

var shouldShowCameraPreview by rememberSaveable { mutableStateOf(false) }
var shouldShowVerificationDialog by rememberSaveable { mutableStateOf(false) }
var shouldShowManualEntryDialog by rememberSaveable { mutableStateOf(false) }

val hasCamera = context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
val cameraPermissionLauncher = rememberLauncherForActivityResult(
contract = RequestPermission(),
onResult = { shouldShowCameraPreview = it }
)

val startActivityForResult = rememberLauncherForActivityResult(
contract = PickVisualMedia(),
onResult = { uri ->
Expand All @@ -189,6 +212,16 @@ private fun RequestTicket(
)
}

if (shouldShowCameraPreview) {
CameraPreviewScreen(
onTokenFound = {
viewModel.getAttendee(eventConfig!!.id, it)
shouldShowCameraPreview = false
},
onDismiss = { shouldShowCameraPreview = false }
)
}

Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
Expand Down Expand Up @@ -223,8 +256,21 @@ private fun RequestTicket(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally)
) {
if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
Button(onClick = {}) {
if (hasCamera) {
Button(
onClick = {
val hasCameraPerm = ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED

if (hasCameraPerm) {
shouldShowCameraPreview = true
} else {
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
) {
Text(text = stringResource(R.string.scan))
}
}
Expand Down Expand Up @@ -394,3 +440,67 @@ private fun ManualEntryDialog(onConfirm: (token: String) -> Unit = {}, onDismiss
}
)
}

@Composable
fun CameraPreviewScreen(onTokenFound: (token: String) -> Unit = {}, onDismiss: () -> Unit = {}) {
val lensFacing = CameraSelector.LENS_FACING_BACK
val lifecycleOwner = LocalLifecycleOwner.current
val context = LocalContext.current
val previewView = remember { PreviewView(context) }

val preview = Preview.Builder()
.build()
val cameraxSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
val reader = BarcodeReader().apply {
options.tryRotate = true
}

val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build().apply {
setAnalyzer(ContextCompat.getMainExecutor(context)) { imageProxy ->
reader.read(imageProxy).firstOrNull()?.let { result ->
if (result.format == BarcodeReader.Format.QR_CODE || !result.text.isNullOrBlank()) {
onTokenFound(result.text!!)
}
}
imageProxy.close()
}
}

LaunchedEffect(key1 = lensFacing) {
val cameraProvider = getCameraProvider(context)
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(lifecycleOwner, cameraxSelector, imageAnalysis, preview)
preview.surfaceProvider = previewView.surfaceProvider
}

Dialog(
onDismissRequest = {

onDismiss()
},
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = false,
decorFitsSystemWindows = true,
usePlatformDefaultWidth = false
),
) {
Surface(modifier = Modifier.fillMaxSize()) {
AndroidView(factory = { previewView }, modifier = Modifier.fillMaxSize())
}
}
}

private suspend fun getCameraProvider(context: Context): ProcessCameraProvider {
return suspendCoroutine { continuation ->
ProcessCameraProvider.getInstance(context).also { cameraProvider ->
cameraProvider.addListener({
continuation.resume(cameraProvider.get())
}, ContextCompat.getMainExecutor(context))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2024 OPass
* SPDX-License-Identifier: GPL-3.0-only
*/

package app.opass.ccip.android.utils

import android.util.Log
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.zxing.BarcodeFormat
import com.google.zxing.Result
import com.google.zxing.client.result.ResultParser
import zxingcpp.BarcodeReader

class QrImageAnalyzer(onTokenFound: (token: String) -> Unit = {}): ImageAnalysis.Analyzer {

private val reader = BarcodeReader().apply {
options.tryRotate = true
}

override fun analyze(image: ImageProxy) {
image.use {
reader.read(image).firstOrNull()?.let { rawResult ->
if (rawResult.format != BarcodeReader.Format.QR_CODE || rawResult.text.isNullOrBlank()) return

val result = Result(
rawResult.text,
rawResult.bytes,
null,
BarcodeFormat.QR_CODE
)
val parsedResult = ResultParser.parseResult(result)
Log.d("AAYUSH", "TOKEN: ${parsedResult.displayResult}, ${rawResult.text}")
}
}
}
}
5 changes: 4 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotl
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
androidx-adaptive-android = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "androidx-adaptiveAndroid" }
androidx-browser = { module = "androidx.browser:browser", version.ref = "androidx-browser" }
androidx-camera = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" }
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidx-camera" }
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-camera" }
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" }
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" }
androidx-hilt-navigation = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidx-hilt" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-composeNavigation" }
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
Expand Down

0 comments on commit 09cd734

Please sign in to comment.