diff --git a/feature/homeImpl/build.gradle.kts b/feature/homeImpl/build.gradle.kts
index 4314ace..55eb848 100644
--- a/feature/homeImpl/build.gradle.kts
+++ b/feature/homeImpl/build.gradle.kts
@@ -12,6 +12,8 @@ dependencies {
implementation(libs.bundles.exoplayer)
implementation(libs.bundles.camerax)
+ implementation(libs.camerax.mlkit)
+ implementation(libs.gms.mlkit.barcode)
implementation(libs.glide.compose)
}
diff --git a/feature/homeImpl/src/main/AndroidManifest.xml b/feature/homeImpl/src/main/AndroidManifest.xml
index f9618a5..15d7e8a 100644
--- a/feature/homeImpl/src/main/AndroidManifest.xml
+++ b/feature/homeImpl/src/main/AndroidManifest.xml
@@ -4,4 +4,10 @@
android:name="android.hardware.camera"
android:required="false" />
+
+
+
+
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt
index 62b9464..d52fb5d 100644
--- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt
@@ -4,9 +4,13 @@ import android.graphics.Bitmap
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavType
import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
import com.featuremodule.core.navigation.HIDE_NAV_BAR
import com.featuremodule.homeApi.HomeDestination
+import com.featuremodule.homeImpl.barcode.BarcodeCameraScreen
+import com.featuremodule.homeImpl.barcode.BarcodeResultScreen
import com.featuremodule.homeImpl.camera.TakePhotoScreen
import com.featuremodule.homeImpl.exoplayer.ExoplayerScreen
import com.featuremodule.homeImpl.imageUpload.ImageUploadScreen
@@ -31,6 +35,20 @@ fun NavGraphBuilder.registerHome() {
composable(InternalRoutes.TakePhotoDestination.ROUTE) {
TakePhotoScreen()
}
+
+ composable(InternalRoutes.BarcodeCameraDestination.ROUTE) {
+ BarcodeCameraScreen()
+ }
+
+ composable(
+ InternalRoutes.BarcodeResultDestination.ROUTE,
+ InternalRoutes.BarcodeResultDestination.arguments,
+ ) {
+ val barcode = it.arguments
+ ?.getString(InternalRoutes.BarcodeResultDestination.ARG_BARCODE)
+ ?: "NONE"
+ BarcodeResultScreen(barcode)
+ }
}
internal class InternalRoutes {
@@ -52,4 +70,21 @@ internal class InternalRoutes {
fun constructRoute() = ROUTE
}
+
+ object BarcodeCameraDestination {
+ const val ROUTE = HIDE_NAV_BAR + "barcode"
+
+ fun constructRoute() = ROUTE
+ }
+
+ object BarcodeResultDestination {
+ const val ARG_BARCODE = "barcode"
+ const val ROUTE = "barcode_result/{$ARG_BARCODE}"
+
+ val arguments = listOf(
+ navArgument(ARG_BARCODE) { type = NavType.StringType },
+ )
+
+ fun constructRoute(barcodeValue: String) = "barcode_result/$barcodeValue"
+ }
}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/barcode/BarcodeCameraScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/barcode/BarcodeCameraScreen.kt
new file mode 100644
index 0000000..794b712
--- /dev/null
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/barcode/BarcodeCameraScreen.kt
@@ -0,0 +1,136 @@
+package com.featuremodule.homeImpl.barcode
+
+import android.Manifest
+import android.content.Context
+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.mlkit.vision.MlKitAnalyzer
+import androidx.camera.view.CameraController
+import androidx.camera.view.LifecycleCameraController
+import androidx.camera.view.PreviewView
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+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.viewinterop.AndroidView
+import androidx.core.content.ContextCompat
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.LifecycleOwner
+import com.google.mlkit.vision.barcode.BarcodeScannerOptions
+import com.google.mlkit.vision.barcode.BarcodeScanning
+import com.google.mlkit.vision.barcode.common.Barcode
+
+@Composable
+internal fun BarcodeCameraScreen(viewModel: BarcodeVM = hiltViewModel()) {
+ val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ var cameraViewVisibility by remember { mutableStateOf(false) }
+ val launchCameraPermissionRequest =
+ rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestPermission(),
+ ) { isGranted ->
+ if (isGranted) {
+ cameraViewVisibility = true
+ } else {
+ viewModel.postEvent(Event.PopBack)
+ }
+ }
+
+ val cameraController = remember {
+ createCameraController(
+ context = context,
+ lifecycleOwner = lifecycleOwner,
+ onBarcodeReceived = { viewModel.postEvent(Event.BarcodeReceived(it)) },
+ )
+ }
+
+ val previewView = remember {
+ PreviewView(context).apply {
+ scaleType = PreviewView.ScaleType.FIT_CENTER
+ controller = cameraController
+ }
+ }
+
+ LaunchedEffect(context) {
+ if (ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.CAMERA,
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ launchCameraPermissionRequest.launch(Manifest.permission.CAMERA)
+ } else {
+ cameraViewVisibility = true
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Black)
+ .windowInsetsPadding(WindowInsets.navigationBars),
+ ) {
+ if (cameraViewVisibility) {
+ AndroidView(
+ factory = { previewView },
+ modifier = Modifier
+ .align(Alignment.Center)
+ .aspectRatio(1f)
+ .fillMaxSize(),
+ )
+ }
+ }
+}
+
+private fun createCameraController(
+ context: Context,
+ lifecycleOwner: LifecycleOwner,
+ onBarcodeReceived: (Barcode) -> Unit,
+): LifecycleCameraController {
+ val barcodeScanner = BarcodeScanning.getClient(
+ BarcodeScannerOptions.Builder()
+ .setBarcodeFormats(
+ Barcode.FORMAT_QR_CODE,
+ Barcode.FORMAT_DATA_MATRIX,
+ Barcode.FORMAT_EAN_13,
+ Barcode.FORMAT_EAN_8,
+ )
+ .build(),
+ )
+
+ return LifecycleCameraController(context).apply {
+ bindToLifecycle(lifecycleOwner)
+ setEnabledUseCases(CameraController.IMAGE_ANALYSIS)
+ cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+
+ setImageAnalysisAnalyzer(
+ ContextCompat.getMainExecutor(context),
+ MlKitAnalyzer(
+ listOf(barcodeScanner),
+ ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED,
+ ContextCompat.getMainExecutor(context),
+ ) { result ->
+ result?.getValue(barcodeScanner)?.firstOrNull()?.let {
+ onBarcodeReceived(it)
+ }
+ },
+ )
+ }
+}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/barcode/BarcodeContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/barcode/BarcodeContract.kt
new file mode 100644
index 0000000..a003cca
--- /dev/null
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/barcode/BarcodeContract.kt
@@ -0,0 +1,12 @@
+package com.featuremodule.homeImpl.barcode
+
+import com.featuremodule.core.ui.UiEvent
+import com.featuremodule.core.ui.UiState
+import com.google.mlkit.vision.barcode.common.Barcode
+
+internal class State : UiState
+
+internal sealed interface Event : UiEvent {
+ data object PopBack : Event
+ data class BarcodeReceived(val barcode: Barcode) : Event
+}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/barcode/BarcodeResultScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/barcode/BarcodeResultScreen.kt
new file mode 100644
index 0000000..c0d32d4
--- /dev/null
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/barcode/BarcodeResultScreen.kt
@@ -0,0 +1,60 @@
+package com.featuremodule.homeImpl.barcode
+
+import android.content.Intent
+import android.os.Build
+import android.widget.Toast
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.unit.dp
+
+@Composable
+internal fun BarcodeResultScreen(barcode: String) {
+ val context = LocalContext.current
+ val clipboard = LocalClipboardManager.current
+
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ SelectionContainer {
+ Text(text = barcode)
+ }
+
+ Button(
+ onClick = {
+ clipboard.setText(AnnotatedString(barcode))
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
+ Toast.makeText(context, "Copied $barcode", Toast.LENGTH_SHORT).show()
+ }
+ },
+ modifier = Modifier.defaultMinSize(minWidth = 100.dp),
+ ) {
+ Text(text = "Copy")
+ }
+
+ Button(
+ onClick = {
+ val shareIntent = Intent(Intent.ACTION_SEND).apply {
+ type = "text/plain"
+ putExtra(Intent.EXTRA_TEXT, barcode)
+ }
+ context.startActivity(Intent.createChooser(shareIntent, null))
+ },
+ modifier = Modifier.defaultMinSize(minWidth = 100.dp),
+ ) {
+ Text(text = "Share")
+ }
+ }
+}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/barcode/BarcodeVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/barcode/BarcodeVM.kt
new file mode 100644
index 0000000..059290d
--- /dev/null
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/barcode/BarcodeVM.kt
@@ -0,0 +1,42 @@
+package com.featuremodule.homeImpl.barcode
+
+import com.featuremodule.core.navigation.NavCommand
+import com.featuremodule.core.navigation.NavManager
+import com.featuremodule.core.ui.BaseVM
+import com.featuremodule.homeImpl.InternalRoutes
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.delay
+import java.util.concurrent.atomic.AtomicBoolean
+import javax.inject.Inject
+
+@HiltViewModel
+internal class BarcodeVM @Inject constructor(
+ private val navManager: NavManager,
+) : BaseVM() {
+ override fun initialState() = State()
+
+ // Throttling due to barcode reader sending multiple results before closing camera
+ private val isBarcodeProcessing = AtomicBoolean(false)
+
+ override fun handleEvent(event: Event) {
+ when (event) {
+ Event.PopBack -> launch { navManager.navigate(NavCommand.PopBack) }
+
+ is Event.BarcodeReceived -> launch {
+ if (isBarcodeProcessing.getAndSet(true)) return@launch
+
+ navManager.navigate(
+ NavCommand.Forward(
+ InternalRoutes.BarcodeResultDestination.constructRoute(
+ event.barcode.displayValue.toString(),
+ ),
+ ),
+ )
+
+ // Throttle time, can be adjusted as needed
+ delay(timeMillis = 5000L)
+ isBarcodeProcessing.set(false)
+ }
+ }
+ }
+}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt
index 3892fac..6cdbe4c 100644
--- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt
@@ -9,4 +9,5 @@ internal sealed interface Event : UiEvent {
data object NavigateToFeatureA : Event
data object NavigateToExoplayer : Event
data object NavigateToCamera : Event
+ data object NavigateToBarcode : Event
}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt
index ec64729..e52ca50 100644
--- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt
@@ -39,6 +39,7 @@ internal fun HomeScreen(route: String?, viewModel: HomeVM = hiltViewModel()) {
GenericButton(text = "Pass number") { viewModel.postEvent(Event.NavigateToFeatureA) }
GenericButton(text = "Exoplayer") { viewModel.postEvent(Event.NavigateToExoplayer) }
GenericButton(text = "Camera") { viewModel.postEvent(Event.NavigateToCamera) }
+ GenericButton(text = "Barcode") { viewModel.postEvent(Event.NavigateToBarcode) }
}
}
}
diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt
index afe3d54..af6c8bc 100644
--- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt
+++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt
@@ -39,6 +39,12 @@ internal class HomeVM @Inject constructor(
NavCommand.Forward(InternalRoutes.ImageUploadDestination.constructRoute()),
)
}
+
+ Event.NavigateToBarcode -> launch {
+ navManager.navigate(
+ NavCommand.Forward(InternalRoutes.BarcodeCameraDestination.constructRoute()),
+ )
+ }
}
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b240345..05443fc 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -24,6 +24,7 @@ glide-compose = "1.0.0-beta01"
leakcanary = "2.14"
media3 = "1.4.1"
camerax = "1.4.0"
+gms-mlkit = "18.3.1"
# Versions used for android{} setup
sdk-compile = "34"
@@ -77,6 +78,8 @@ camerax-core = { module = "androidx.camera:camera-core", version.ref = "camerax"
camerax-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" }
camerax-view = { module = "androidx.camera:camera-view", version.ref = "camerax" }
camerax-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" }
+camerax-mlkit = { module = "androidx.camera:camera-mlkit-vision", version.ref = "camerax" }
+gms-mlkit-barcode = { module = "com.google.android.gms:play-services-mlkit-barcode-scanning", version.ref = "gms-mlkit" }
# Testing
junit = { module = "junit:junit", version.ref = "junit" }