Skip to content

Commit

Permalink
Merge pull request #20100 from wordpress-mobile/issue/add-support-for…
Browse files Browse the repository at this point in the history
…-mlkit

Finish migration of scanning library from Google Code Scanner to MLKIT
  • Loading branch information
AjeshRPai authored Feb 3, 2024
2 parents b922c8a + 175ab13 commit 2be635d
Show file tree
Hide file tree
Showing 17 changed files with 949 additions and 28 deletions.
10 changes: 8 additions & 2 deletions WordPress/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -423,8 +423,14 @@ dependencies {
implementation "com.google.android.play:review:$googlePlayReviewVersion"
implementation "com.google.android.play:review-ktx:$googlePlayReviewVersion"
implementation "com.google.android.gms:play-services-auth:$googlePlayServicesAuthVersion"
implementation "com.google.android.gms:play-services-code-scanner:$googlePlayServicesCodeScannerVersion"
implementation "com.google.mlkit:barcode-scanning-common:$googleMLKitBarcodeScanningVersion"
implementation "com.google.mlkit:barcode-scanning-common:$googleMLKitBarcodeScanningCommonVersion"
implementation "com.google.mlkit:text-recognition:$googleMLKitTextRecognitionVersion"
implementation "com.google.mlkit:barcode-scanning:$googleMLKitBarcodeScanningVersion"

// CameraX
implementation "androidx.camera:camera-camera2:$androidxCameraVersion"
implementation "androidx.camera:camera-lifecycle:$androidxCameraVersion"
implementation "androidx.camera:camera-view:$androidxCameraVersion"

implementation "com.android.installreferrer:installreferrer:$androidInstallReferrerVersion"
implementation "com.github.chrisbanes:PhotoView:$chrisbanesPhotoviewVersion"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.wordpress.android.modules

import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScanning
import dagger.Module
import dagger.Provides
import dagger.Reusable
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.wordpress.android.ui.barcodescanner.CodeScanner
import org.wordpress.android.ui.barcodescanner.GoogleBarcodeFormatMapper
import org.wordpress.android.ui.barcodescanner.GoogleCodeScannerErrorMapper
import org.wordpress.android.ui.barcodescanner.GoogleMLKitCodeScanner
import org.wordpress.android.ui.barcodescanner.MediaImageProvider

@InstallIn(SingletonComponent::class)
@Module
class CodeScannerModule {
@Provides
@Reusable
fun provideGoogleCodeScanner(
barcodeScanner: BarcodeScanner,
googleCodeScannerErrorMapper: GoogleCodeScannerErrorMapper,
barcodeFormatMapper: GoogleBarcodeFormatMapper,
inputImageProvider: MediaImageProvider,
): CodeScanner {
return GoogleMLKitCodeScanner(
barcodeScanner,
googleCodeScannerErrorMapper,
barcodeFormatMapper,
inputImageProvider,
)
}

@Provides
@Reusable
fun providesGoogleBarcodeScanner() = BarcodeScanning.getClient()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.wordpress.android.ui.barcodescanner

import android.content.res.Configuration
import android.util.Size
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
import androidx.camera.core.ImageProxy
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.wordpress.android.ui.compose.theme.AppTheme
import androidx.camera.core.Preview as CameraPreview

@Composable
fun BarcodeScanner(
codeScanner: CodeScanner,
onScannedResult: (Flow<CodeScannerStatus>) -> Unit
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val cameraProviderFuture = remember {
ProcessCameraProvider.getInstance(context)
}
Column(
modifier = Modifier.fillMaxSize()
) {
AndroidView(
factory = { context ->
val previewView = PreviewView(context)
val preview = CameraPreview.Builder().build()
preview.setSurfaceProvider(previewView.surfaceProvider)
val selector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
val imageAnalysis = ImageAnalysis.Builder().setTargetResolution(
Size(
previewView.width,
previewView.height
)
)
.setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(context)) { imageProxy ->
onScannedResult(codeScanner.startScan(imageProxy))
}
try {
cameraProviderFuture.get().bindToLifecycle(lifecycleOwner, selector, preview, imageAnalysis)
} catch (e: IllegalStateException) {
onScannedResult(
flowOf(
CodeScannerStatus.Failure(
e.message
?: "Illegal state exception while binding camera provider to lifecycle",
CodeScanningErrorType.Other(e)
)
)
)
} catch (e: IllegalArgumentException) {
onScannedResult(
flowOf(
CodeScannerStatus.Failure(
e.message
?: "Illegal argument exception while binding camera provider to lifecycle",
CodeScanningErrorType.Other(e)
)
)
)
}
previewView
},
modifier = Modifier.fillMaxSize()
)
}
}

class DummyCodeScanner : CodeScanner {
override fun startScan(imageProxy: ImageProxy): Flow<CodeScannerStatus> {
return flowOf(CodeScannerStatus.Success("", GoogleBarcodeFormatMapper.BarcodeFormat.FormatUPCA))
}
}

@Preview(name = "Light mode")
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun BarcodeScannerScreenPreview() {
AppTheme {
BarcodeScanner(codeScanner = DummyCodeScanner(), onScannedResult = {})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package org.wordpress.android.ui.barcodescanner

import android.content.res.Configuration
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.padding
import androidx.compose.material.AlertDialog
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.wordpress.android.R
import kotlinx.coroutines.flow.Flow
import org.wordpress.android.ui.compose.theme.AppTheme

@Composable
fun BarcodeScannerScreen(
codeScanner: CodeScanner,
permissionState: BarcodeScanningViewModel.PermissionState,
onResult: (Boolean) -> Unit,
onScannedResult: (Flow<CodeScannerStatus>) -> Unit,
) {
val cameraPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = { granted ->
onResult(granted)
},
)
LaunchedEffect(key1 = Unit) {
cameraPermissionLauncher.launch(BarcodeScanningFragment.KEY_CAMERA_PERMISSION)
}
when (permissionState) {
BarcodeScanningViewModel.PermissionState.Granted -> {
BarcodeScanner(
codeScanner = codeScanner,
onScannedResult = onScannedResult
)
}
is BarcodeScanningViewModel.PermissionState.ShouldShowRationale -> {
AlertDialog(
title = stringResource(id = permissionState.title),
message = stringResource(id = permissionState.message),
ctaLabel = stringResource(id = permissionState.ctaLabel),
dismissCtaLabel = stringResource(id = permissionState.dismissCtaLabel),
ctaAction = { permissionState.ctaAction.invoke(cameraPermissionLauncher) },
dismissCtaAction = { permissionState.dismissCtaAction.invoke() }
)
}
is BarcodeScanningViewModel.PermissionState.PermanentlyDenied -> {
AlertDialog(
title = stringResource(id = permissionState.title),
message = stringResource(id = permissionState.message),
ctaLabel = stringResource(id = permissionState.ctaLabel),
dismissCtaLabel = stringResource(id = permissionState.dismissCtaLabel),
ctaAction = { permissionState.ctaAction.invoke(cameraPermissionLauncher) },
dismissCtaAction = { permissionState.dismissCtaAction.invoke() }
)
}
BarcodeScanningViewModel.PermissionState.Unknown -> {
// no-op
}
}
}

@Composable
private fun AlertDialog(
title: String,
message: String,
ctaLabel: String,
dismissCtaLabel: String,
ctaAction: () -> Unit,
dismissCtaAction: () -> Unit,
) {
AlertDialog(
onDismissRequest = { dismissCtaAction() },
title = {
Text(title)
},
text = {
Text(message)
},
confirmButton = {
TextButton(
onClick = {
ctaAction()
}
) {
Text(
ctaLabel,
color = MaterialTheme.colors.secondary,
modifier = Modifier.padding(8.dp)
)
}
},
dismissButton = {
TextButton(
onClick = {
dismissCtaAction()
}
) {
Text(
dismissCtaLabel,
color = MaterialTheme.colors.secondary,
modifier = Modifier.padding(8.dp)
)
}
},
)
}

@Preview(name = "Light mode")
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun DeniedOnceAlertDialog() {
AppTheme {
AlertDialog(
title = stringResource(id = R.string.barcode_scanning_alert_dialog_title),
message = stringResource(id = R.string.barcode_scanning_alert_dialog_rationale_message),
ctaLabel = stringResource(id = R.string.barcode_scanning_alert_dialog_rationale_cta_label),
dismissCtaLabel = stringResource(id = R.string.barcode_scanning_alert_dialog_dismiss_label),
ctaAction = {},
dismissCtaAction = {},
)
}
}

@Preview(name = "Light mode")
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun DeniedPermanentlyAlertDialog() {
AppTheme {
AlertDialog(
title = stringResource(id = R.string.barcode_scanning_alert_dialog_title),
message = stringResource(id = R.string.barcode_scanning_alert_dialog_permanently_denied_message),
ctaLabel = stringResource(id = R.string.barcode_scanning_alert_dialog_permanently_denied_cta_label),
dismissCtaLabel = stringResource(id = R.string.barcode_scanning_alert_dialog_dismiss_label),
ctaAction = {},
dismissCtaAction = {},
)
}
}
Loading

0 comments on commit 2be635d

Please sign in to comment.