TP-65206 | QR Scanner performance improvement (#10691)

Co-authored-by: Shaurya Rehan <shaurya.rehan@navi.com>
Co-authored-by: Shivam Goyal <shivam.goyal@navi.com>
This commit is contained in:
Ujjwal Kumar
2024-05-17 18:16:07 +05:30
committed by GitHub
parent e1c42e7aea
commit a7d1e6042a
4 changed files with 157 additions and 63 deletions

View File

@@ -20,7 +20,8 @@ androidx-annotation = "1.7.1"
androidx-appcompat = "1.6.1"
androidx-arch-core-testing = "2.2.0"
androidx-browser = "1.3.0"
androidx-camera = "1.3.0-beta01"
androidx-camera = "1.3.3"
androidx-camera-mlkit = "1.3.0-beta02"
androidx-constraintlayout = "2.1.4"
androidx-constraintlayoutCompose = "1.1.0-alpha10"
androidx-core-ktx = "1.8.0"
@@ -160,7 +161,7 @@ androidx-browser = { module = "androidx.browser:browser", version.ref = "android
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-camera" }
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidx-camera" }
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" }
androidx-camera-mlkit-vision = { module = "androidx.camera:camera-mlkit-vision", version.ref = "androidx-camera" }
androidx-camera-mlkit-vision = { module = "androidx.camera:camera-mlkit-vision", version.ref = "androidx-camera-mlkit" }
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }

View File

@@ -78,6 +78,7 @@ dependencies {
implementation libs.android.play.core.ktx
implementation libs.androidx.appcompat
implementation libs.androidx.camera.mlkit.vision
implementation libs.androidx.compose.runtime.livedata
implementation libs.androidx.constraintlayout
implementation libs.androidx.core.google.shortcuts
implementation libs.androidx.core.ktx

View File

@@ -7,17 +7,21 @@
package com.navi.pay.management.moneytransfer.scanpay.ui
import android.util.Size
import android.view.ScaleGestureDetector
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.Camera
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.core.ZoomState
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.mlkit.vision.MlKitAnalyzer
import androidx.camera.view.CameraController
import androidx.camera.view.CameraController.IMAGE_ANALYSIS
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -38,6 +42,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
@@ -58,15 +63,16 @@ import androidx.core.content.ContextCompat
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LiveData
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_QR_CODE
import com.google.mlkit.vision.common.InputImage
import com.navi.alfred.AlfredManager
import com.navi.base.utils.EMPTY
import com.navi.common.utils.log
import com.navi.design.font.FontWeightEnum
import com.navi.design.snackbar.NaviSnackBar
import com.navi.design.snackbar.SnackBarConfig
@@ -112,6 +118,7 @@ import com.ramcosta.composedestinations.result.ResultRecipient
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@androidx.annotation.OptIn(ExperimentalCameraProviderConfiguration::class)
@Destination
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterialApi::class)
@Composable
@@ -135,12 +142,6 @@ fun QrScannerScreen(
Unit
}
val options = remember {
BarcodeScannerOptions.Builder().setBarcodeFormats(Barcode.FORMAT_QR_CODE).build()
}
val barcodeScanner = remember { BarcodeScanning.getClient(options) }
val qrImageErrorViewEnable by
qrScannerViewModel.qrImageErrorViewEnable.collectAsStateWithLifecycle()
val rewardsNudgeDetailEntity by
@@ -151,7 +152,11 @@ fun QrScannerScreen(
uri?.let {
try {
val image = InputImage.fromFilePath(naviPayActivity, uri)
barcodeScanner
BarcodeScanning.getClient(
BarcodeScannerOptions.Builder()
.setBarcodeFormats(FORMAT_QR_CODE)
.build()
)
.process(image)
.addOnSuccessListener { barcodes ->
if ((barcodes != null) && barcodes.size > 0) {
@@ -236,7 +241,7 @@ fun QrScannerScreen(
}
}
val isTorchEnabled by qrScannerViewModel.isTorchEnabled.collectAsStateWithLifecycle()
val isTorchEnabled by qrScannerViewModel.isTorchEnabled.observeAsState(false)
val showPopUpPermission by qrScannerViewModel.showPopUpPermission.collectAsStateWithLifecycle()
val cameraPermissionsState =
@@ -403,10 +408,9 @@ fun QrScannerScreen(
if (cameraPermissionsState.allPermissionsGranted) {
QrCamera(
naviPayAnalytics = naviPayAnalytics,
isTorchEnabled = isTorchEnabled,
isTorchEnabled = qrScannerViewModel.isTorchEnabled,
naviPayActivity = naviPayActivity,
onScanSuccess = qrScannerViewModel::processQrContent,
barcodeScanner = barcodeScanner,
naviPaySessionId = qrScannerViewModel.getNaviPaySessionId()
)
}
@@ -494,13 +498,13 @@ fun QrScannerScreen(
}
}
@ExperimentalCameraProviderConfiguration
@Composable
private fun QrCamera(
isTorchEnabled: Boolean,
isTorchEnabled: LiveData<Boolean>,
naviPayActivity: NaviPayActivity,
onScanSuccess: (String) -> Unit,
naviPayAnalytics: NaviPayAnalytics.NaviPayQrScanner,
barcodeScanner: BarcodeScanner,
naviPaySessionId: String?
) {
val context = LocalContext.current
@@ -520,51 +524,137 @@ private fun QrCamera(
previewView
}
val cameraController = remember {
LifecycleCameraController(naviPayActivity.baseContext).apply {
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
isPinchToZoomEnabled = true
isTapToFocusEnabled = true
imageAnalysisTargetSize = CameraController.OutputSize(Size(2600, 2160))
imageAnalysisBackpressureStrategy = ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
setEnabledUseCases(IMAGE_ANALYSIS)
setLinearZoom(0.3f)
setImageAnalysisAnalyzer(
ContextCompat.getMainExecutor(naviPayActivity.baseContext),
MlKitAnalyzer(
listOf(barcodeScanner),
CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED,
ContextCompat.getMainExecutor(naviPayActivity.baseContext)
) { result: MlKitAnalyzer.Result ->
val barcodeResults = result.getValue(barcodeScanner)
if ((barcodeResults != null) && barcodeResults.size > 0) {
onScanSuccess(barcodeResults.first().rawValue.toString())
naviPayAnalytics.onQRReceived(
isFromGallery = false,
qrCount = barcodeResults.size,
naviPaySessionId = naviPaySessionId
)
}
}
)
bindToLifecycle(lifecycleOwner)
previewView.controller = this
}
}
LaunchedEffect(cameraController, isTorchEnabled) {
if (cameraController.cameraInfo?.hasFlashUnit() == true) {
cameraController.cameraControl?.enableTorch(isTorchEnabled)
}
}
AndroidView(
factory = { previewView },
factory = {
ProcessCameraProvider.getInstance(naviPayActivity.applicationContext).apply {
addListener(
{
val cameraProvider = this.get()
val cameraSelector =
CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
val barcodeScanner =
BarcodeScanning.getClient(
BarcodeScannerOptions.Builder()
.setBarcodeFormats(FORMAT_QR_CODE)
.build()
)
val imageAnalyzer =
ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
.setResolutionSelector(
ResolutionSelector.Builder()
.setAllowedResolutionMode(
ResolutionSelector
.PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
)
.build()
)
.build()
.also {
it.setAnalyzer(
ContextCompat.getMainExecutor(naviPayActivity.baseContext),
MlKitAnalyzer(
listOf(barcodeScanner),
ImageAnalysis.COORDINATE_SYSTEM_ORIGINAL,
ContextCompat.getMainExecutor(
naviPayActivity.baseContext
)
) { result: MlKitAnalyzer.Result ->
val barcodeResults = result.getValue(barcodeScanner)
if (
barcodeResults != null &&
barcodeResults.size > 0 &&
!barcodeResults.first().rawValue.isNullOrEmpty()
) {
onScanSuccess(
barcodeResults.first().rawValue.toString()
)
naviPayAnalytics.onQRReceived(
isFromGallery = false,
qrCount = barcodeResults.size,
naviPaySessionId = naviPaySessionId
)
}
}
)
}
val preview =
Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
var camera: Camera? = null
try {
cameraProvider.unbindAll()
camera =
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageAnalyzer
)
} catch (e: Exception) {
e.log()
}
val scaleGestureDetector =
ScaleGestureDetector(
context,
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
updateZoom(
scaleFactor = detector.scaleFactor,
cameraControl = camera?.cameraControl,
zoomState = camera?.cameraInfo?.zoomState?.value
)
return true
}
}
)
previewView.setOnTouchListener { _, event ->
scaleGestureDetector.onTouchEvent(event)
true
}
isTorchEnabled.observe(lifecycleOwner) { isTorchEnabled ->
if (camera?.cameraInfo?.hasFlashUnit() == true) {
camera.cameraControl.enableTorch(isTorchEnabled)
}
}
},
ContextCompat.getMainExecutor(context)
)
}
previewView
},
modifier = Modifier.fillMaxSize().background(color = NaviPayColor.darkGray)
)
}
private fun updateZoom(scaleFactor: Float, cameraControl: CameraControl?, zoomState: ZoomState?) {
if (cameraControl == null || zoomState == null) {
return
}
val currentZoomRatio = zoomState.zoomRatio
val newZoomRatio = currentZoomRatio * scaleFactor
// Ensure zoom stays within valid bounds
val minZoomRatio = zoomState.minZoomRatio
val maxZoomRatio = zoomState.maxZoomRatio
val adjustedZoomRatio = minZoomRatio.coerceAtLeast(newZoomRatio.coerceAtMost(maxZoomRatio))
cameraControl.setZoomRatio(adjustedZoomRatio)
}
@Composable
private fun TorchButton(isTorchEnabled: Boolean, onTorchToggle: () -> Unit) {
Image(

View File

@@ -7,6 +7,8 @@
package com.navi.pay.management.moneytransfer.scanpay.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.navi.base.utils.ResourceProvider
import com.navi.common.network.models.isSuccessWithData
@@ -77,8 +79,8 @@ constructor(
private val _qrImageErrorViewEnable = MutableStateFlow(false)
val qrImageErrorViewEnable = _qrImageErrorViewEnable.asStateFlow()
private val _isTorchEnabled = MutableStateFlow(false)
val isTorchEnabled = _isTorchEnabled.asStateFlow()
private val _isTorchEnabled = MutableLiveData<Boolean>(false)
val isTorchEnabled = _isTorchEnabled as LiveData<Boolean>
var isQrCodeProcessing: AtomicBoolean = AtomicBoolean(false)
@@ -223,7 +225,7 @@ constructor(
}
fun toggleTorchStatus() {
viewModelScope.launch { _isTorchEnabled.update { !isTorchEnabled.value } }
viewModelScope.launch { _isTorchEnabled.value = !(isTorchEnabled.value ?: false) }
}
fun setImageErrorView(qrImageErrorViewEnable: Boolean) {