Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions feature/homeImpl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
6 changes: 6 additions & 0 deletions feature/homeImpl/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />

<application>
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode" />
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
},
)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
Original file line number Diff line number Diff line change
@@ -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<State, Event>() {
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)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ internal class HomeVM @Inject constructor(
NavCommand.Forward(InternalRoutes.ImageUploadDestination.constructRoute()),
)
}

Event.NavigateToBarcode -> launch {
navManager.navigate(
NavCommand.Forward(InternalRoutes.BarcodeCameraDestination.constructRoute()),
)
}
}
}
}
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" }
Expand Down
Loading