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:
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user