From 3bab721b5fa4463b6eaa793b669e22ccf6c29b91 Mon Sep 17 00:00:00 2001 From: MichelleWu09 <156372216+MichelleWu09@users.noreply.github.com> Date: Thu, 28 May 2026 13:32:16 -0400 Subject: [PATCH 01/47] barcode map function --- .DS_Store | Bin 0 -> 6148 bytes AISuite_Demos/.DS_Store | Bin 0 -> 6148 bytes .../data/AIDataCaptureDemoUiState.kt | 1 + .../aidatacapturedemo/model/FileUtils.kt | 15 + .../ui/view/AIDataCaptureStartScreen.kt | 12 + .../ui/view/BarcodeMapResultScreen.kt | 277 ++++++++++++++++++ .../ui/view/CameraPreviewScreen.kt | 9 +- .../ui/view/DemoSettingsScreen.kt | 13 +- .../ui/view/DemoStartScreen.kt | 3 +- .../ui/view/GlobalConstants.kt | 18 +- .../ui/view/NavigationStack.kt | 10 + .../viewmodel/AIDataCaptureDemoViewModel.kt | 55 +++- .../app/src/main/res/values/strings.xml | 2 + AISuite_Demos/AIDataCaptureDemo/gradlew | 0 .../zebra/ai/barcodefinder/MainActivity.kt | 6 + .../compose/screens/homescreen/HomeScreen.kt | 219 +++++++++----- 16 files changed, 530 insertions(+), 110 deletions(-) create mode 100644 .DS_Store create mode 100644 AISuite_Demos/.DS_Store create mode 100644 AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt mode change 100644 => 100755 AISuite_Demos/AIDataCaptureDemo/gradlew diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..531cb231781182ada628c328492bb3b2f563d06e GIT binary patch literal 6148 zcmeHKy>1gh5dPLNibaBCQXtY@19XvG(NLVNqo7Q#p^1!t;vZrAENn|C)ja|4kRoj) z-oOPB1rqQ8C`D4`0n+o$?uzWi#y<^;(2lk9-R;iY?0kE-;{~89lV%Ah0w}Nv<{whc z5gC^@BRSjigs9{hy>=;XB>mxxrSI?zcn0nx1M=?>S<%T>CUU(4C?=ECt|=XP(%Jx}S12aMZA6Gy1Ik)c{=*-7{?JFJI@$VCqWs(x-xuZ|!8>X_GJrI>DZ z>-su*0*8_&>(AZgj4|NS4*5G`o|#2mYV0g6zmSWQRRXwVs86-2C1YtUmxy)zG9+xS zU3TT=rWCdedhHqT40s0qFd+Lwz$O@4ObzO-gGwI(h*J5q@LJ9i;uBa5Ev5!JLQ%$* zXk3*&VkqOz{=nLW7E^=99ZE;$b8KW~Pbf-9XMdp0p+bXRdj>oMSq3)DZBgp~>%X7> zvrhibGvFEcuNYAID2^&j$=24HCd5QkEcT210SmE^6!{AU zs}za|f{kY$B(p~IXhdahxN~M_?%kbpc4sC-L@d_{GDImNO2P#umQZ|QqF?xk&FPjj zkjZ1;pH4pba{vfGQN9zpT9y@2zvs^F!{VHOb$4XLk?X z?B|GJLPWD>tPt!8>6E4{?NNbpRH7D@iOaDRXcwbu$aEFEc6}>5jSLZ|8vt^ocG+qrkJc!YTZ^N6Ybs$ZJJb0bB!H*UCms{ASBE__5-l+!vId2jIi;tI|GP@Cxy|Pk}8uH~8W6e7_p-8=GbFyIt=+ z*6!u!ImPqL_cvRn!H;Os#X$d#iOB>@E-SR9Rlq7>6&NTW)&~bJ(A5|zluHK+c?19^ z&@2u0l&An5jjqN>A$nj;r2) { + try { + val timestamp = getTimeStamp() + val fileName = "barcode_layout_$timestamp.json" + val file = File(mContext.getExternalFilesDir(null), fileName) + FileWriter(file).use { writer -> + gson.toJson(barcodeResults, writer) + } + Log.d(TAG, "Barcode results saved to ${file.absolutePath}") + } catch (e: Exception) { + e.printStackTrace() + } + } + private fun getTimeStamp(): String { return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmssSSS")) } diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureStartScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureStartScreen.kt index 00b621b..61d2aac 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureStartScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureStartScreen.kt @@ -287,6 +287,18 @@ fun AIDataCaptureTechnologyList( viewModel.initModel() navController.navigate(route = Screen.DemoStart.route) }) + AIDataCaptureListItem( + R.drawable.barcode_icon, + stringResource(id = R.string.barcode_map_demo), + stringResource(id = R.string.barcode_map_desc), + Variables.mainIcon2, + Variables.secondaryIcon2, + onItemClick = { selectedUsecase -> + viewModel.updateAppBarTitle(getString(context, R.string.barcode_map_demo)) + viewModel.updateSelectedUsecase(selectedUsecase) + viewModel.initModel() + navController.navigate(route = Screen.DemoStart.route) + }) AIDataCaptureListItem( R.drawable.retail_shelf_icon, stringResource(id = R.string.retail_shelf_demo), diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt new file mode 100644 index 0000000..6829764 --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt @@ -0,0 +1,277 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.ui.view + +import android.content.Context +import android.view.WindowManager +import android.view.WindowMetrics +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat.getSystemService +import androidx.navigation.NavController +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import kotlin.math.min + +/** + * BarcodeMapResultScreen is a Composable function that displays the relative positions + * of the detected barcodes and their IDs on a clean background. + */ +@Composable +fun BarcodeMapResultScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + activityInnerPadding: PaddingValues, + context: Context +) { + val uiState = viewModel.uiState.collectAsState().value + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + viewModel.updateAppBarTitle("Barcode Map") + + val capturedBitmap = uiState.captureBitmap + if (capturedBitmap == null) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator() + } + } else { + // GET DEVICE RESOLUTION: + val displayMetrics = LocalContext.current.resources.displayMetrics + val displayMetricsDensity = displayMetrics.density + + val windowManager = getSystemService(context, WindowManager::class.java) + val windowMetrics: WindowMetrics = windowManager.currentWindowMetrics + + val displayTotalWidthInPx = windowMetrics.bounds.width() + val displayTotalHeightInPx = windowMetrics.bounds.height() + + // TOP STATUS BAR + val displayStatusBarPaddingValues = WindowInsets.statusBars.asPaddingValues() + val displayStatusBarHeightInDp = displayStatusBarPaddingValues.calculateTopPadding() + val displayStatusBarHeightInPx = displayStatusBarHeightInDp.value * displayMetricsDensity + + // BOTTOM NAVIGATION BAR + val displayNavigationBarPaddingValues = WindowInsets.navigationBars.asPaddingValues() + val displayNavigationBarHeightInDp = + displayNavigationBarPaddingValues.calculateBottomPadding() + val displayNavigationBarHeightInPx = + displayNavigationBarHeightInDp.value * displayMetricsDensity + + val availableHeightInPx = + displayTotalHeightInPx.toFloat() - displayStatusBarHeightInPx - displayNavigationBarHeightInPx + + // The following computed values are used for drawing Bbox overlay + val scaler = min( + displayTotalWidthInPx.toFloat() / capturedBitmap.width.toFloat(), + availableHeightInPx / capturedBitmap.height.toFloat() + ) + val scaledWidth = scaler * capturedBitmap.width.toFloat() + val scaledHeight = scaler * capturedBitmap.height.toFloat() + val gapX = (displayTotalWidthInPx - scaledWidth) / 2f + val gapY = (availableHeightInPx - scaledHeight) / 2f + + + Box( // Bottom layer + modifier = Modifier + .fillMaxSize() + .padding( + top = displayStatusBarHeightInDp, + bottom = displayNavigationBarHeightInDp + ) + .background(color = Color(0xFFF0F0F0)) // Clean light gray background + ) { + + // MAP CANVAS + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + // We don't draw the captured image anymore as per user request + + DrawBarcodeMapOnCanvas( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + } + + // SUMMARY OVERLAY + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + contentAlignment = Alignment.TopCenter + ) { + Text( + text = "Detected ${uiState.barcodeResults.size} Barcodes", + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ), + modifier = Modifier + .background(Color.White.copy(alpha = 0.8f), RoundedCornerShape(8.dp)) + .padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + + // SAVE BUTTON + Box( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 24.dp), + contentAlignment = Alignment.BottomCenter + ) { + Button( + onClick = { + viewModel.saveBarcodeLayout() + }, + modifier = Modifier + .fillMaxWidth(0.7f) + .height(56.dp), + shape = RoundedCornerShape(28.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF006400)) + ) { + Text( + text = "Save Layout", + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + ) + } + } + } + } +} + +@Composable +private fun DrawBarcodeMapOnCanvas( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float +) { + Canvas( + modifier = Modifier + .fillMaxSize() + ) { + uiState.barcodeResults.forEachIndexed { index, barcodeData -> + barcodeData?.let { + + val bBoxTop = barcodeData.boundingBox.top.toFloat() + val bBoxLeft = barcodeData.boundingBox.left.toFloat() + val bBoxBottom = barcodeData.boundingBox.bottom.toFloat() + val bBoxRight = barcodeData.boundingBox.right.toFloat() + + val scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX + val scaledBBoxTopInPx = (scaler * bBoxTop) + gapY + val scaledBBoxRightInPx = (scaler * bBoxRight) + gapX + val scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + + // Define the size and position of the rectangle + val rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + val rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + + // 1. Draw Bounding Box + drawRect( + color = Color(0xFF00FF00), // zebra green + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (2f * displayMetricsDensity)) + ) + + // 2. Draw semi-transparent overlay inside the box + drawRect( + color = Color(0x3300FF00), + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight) + ) + + // 3. Prepare label text + val idText = if (barcodeData.text.isNotEmpty()) barcodeData.text else "N/A" + val labelText = "#${index + 1}: $idText" + + val paint = android.graphics.Paint().apply { + color = android.graphics.Color.WHITE + textSize = 14f * displayMetricsDensity + typeface = android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD) + isAntiAlias = true + } + + val textWidth = paint.measureText(labelText) + val fontMetrics = paint.fontMetrics + val textHeight = fontMetrics.descent - fontMetrics.ascent + + // Position label above the barcode + val padding = 4f * displayMetricsDensity + val labelX = scaledBBoxLeftInPx + var labelY = scaledBBoxTopInPx - textHeight - (2 * padding) + + // Ensure label is within screen bounds + if (labelY < 0) { + labelY = scaledBBoxBottomInPx + padding + } + + // Draw label background + drawRect( + color = Color(0xFF006400), // Darker green + topLeft = Offset(labelX, labelY), + size = androidx.compose.ui.geometry.Size(textWidth + (2 * padding), textHeight + padding) + ) + + // Draw text + drawContext.canvas.nativeCanvas.drawText( + labelText, + labelX + padding, + labelY + textHeight - fontMetrics.descent, + paint + ) + } + } + } +} diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt index c9eb016..59d0138 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt @@ -156,7 +156,8 @@ fun CameraPreviewScreen( viewModel.updateOcrResultData(results = null) } - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, + UsecaseState.BarcodeMap.value -> { viewModel.updateBarcodeResultData(results = listOf()) } @@ -333,7 +334,8 @@ fun CameraPreviewScreen( ) } - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, + UsecaseState.BarcodeMap.value -> { DrawBarcodeResult( uiState = uiState, scaler = scaler, @@ -1163,6 +1165,7 @@ fun showBottomBar( ) } if ((uiState.usecaseSelected == UsecaseState.Product.value) || + (uiState.usecaseSelected == UsecaseState.BarcodeMap.value) || ((uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value) && (uiState.isCaptureOrLiveEnabled == 0))){ var isClickable = remember { mutableStateOf(true) } Icon( @@ -1189,6 +1192,8 @@ fun showBottomBar( if (uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value) { viewModel.updateOcrBarcodeCaptureSessionCount(uiState.ocrBarcodeCaptureSessionCount + 1) navController.navigate(route = Screen.OCRBarcodeCapture.route) + } else if (uiState.usecaseSelected == UsecaseState.BarcodeMap.value) { + navController.navigate(route = Screen.BarcodeMapResults.route) } else { navController.navigate(route = Screen.ProductsCapture.route) } diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt index 5e9a450..80fc35a 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt @@ -106,7 +106,7 @@ fun DemoSettingsScreen( val settingsItemsList = ExpandableSettingsItemsList() settingsItemsList.AddCommonSettings() - if (demo == UsecaseState.Barcode.value) { + if (demo == UsecaseState.Barcode.value || demo == UsecaseState.BarcodeMap.value) { settingsItemsList.AddBarcodeSettings() } else if (demo == UsecaseState.OCRBarcodeFind.value){ @@ -332,7 +332,7 @@ fun AddIndividualSettings(item: ExpandableSettingsItem, viewModel: AIDataCapture var fileName: String = "" if ((uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value) || (uiState.usecaseSelected == UsecaseState.OCR.value)) { fileName = "ocr_model_input_size.html" - } else if (uiState.usecaseSelected == UsecaseState.Barcode.value) { + } else if (uiState.usecaseSelected == UsecaseState.Barcode.value || uiState.usecaseSelected == UsecaseState.BarcodeMap.value) { fileName = "barcode_model_input_size.html" } else { fileName = "product_model_input_size.html" @@ -350,7 +350,7 @@ fun AddIndividualSettings(item: ExpandableSettingsItem, viewModel: AIDataCapture var fileName: String = "" if ((uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value) || (uiState.usecaseSelected == UsecaseState.OCR.value)) { fileName = "ocr_resolution.html" - } else if (uiState.usecaseSelected == UsecaseState.Barcode.value) { + } else if (uiState.usecaseSelected == UsecaseState.Barcode.value || uiState.usecaseSelected == UsecaseState.BarcodeMap.value) { fileName = "barcode_resolution.html" } else { fileName = "product_resolution.html" @@ -366,7 +366,7 @@ fun AddIndividualSettings(item: ExpandableSettingsItem, viewModel: AIDataCapture horizontalAlignment = Alignment.CenterHorizontally, ) { var fileName: String = "" - if ((uiState.usecaseSelected == UsecaseState.Barcode.value) || (uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value)) { + if ((uiState.usecaseSelected == UsecaseState.Barcode.value) || (uiState.usecaseSelected == UsecaseState.BarcodeMap.value) || (uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value)) { fileName = "barcode_symbologies.html" } val htmlString = viewModel.loadInputStreamFromAsset(fileName = fileName) @@ -552,7 +552,7 @@ fun AddModelInputSizeRadioButtonList(viewModel: AIDataCaptureDemoViewModel) { } ) ) - if (currentUIState.usecaseSelected == UsecaseState.Barcode.value) { + if (currentUIState.usecaseSelected == UsecaseState.Barcode.value || currentUIState.usecaseSelected == UsecaseState.BarcodeMap.value) { // Remove inputSize 2560 option for Barcode Decoder listOfModelInputSizes.removeAt(listOfModelInputSizes.size - 1) } @@ -639,7 +639,8 @@ fun AddAboutInformation(viewModel: AIDataCaptureDemoViewModel) { ) } - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, + UsecaseState.BarcodeMap.value -> { Pair( first = "Barcode Recognizer Version", second = BuildConfig.BarcodeLocalizer_Version diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoStartScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoStartScreen.kt index 06b6f7f..963651a 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoStartScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoStartScreen.kt @@ -675,7 +675,8 @@ private fun LoadingScreen( isStartDisabledChanged(true) } } - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, + UsecaseState.BarcodeMap.value -> { if (uiState.isBarcodeModelDemoReady) { isLoading.value = false isStartDisabledChanged(false) diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt index a0c743b..4b8fa62 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt @@ -88,7 +88,7 @@ object Variables { fun getIconMainColor(demo: String): Color { var mainColor: Color = mainIcon2 when (demo) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { mainColor = mainIcon2 } @@ -115,7 +115,7 @@ fun getIconMainColor(demo: String): Color { fun getIconSecondaryColor(demo: String): Color { var secondaryColor: Color = secondaryIcon2 when (demo) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { secondaryColor = secondaryIcon2 } @@ -141,7 +141,7 @@ fun getIconSecondaryColor(demo: String): Color { fun getIconId(demo: String): Int? { var iconId: Int? = null when (demo) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { iconId = R.drawable.barcode_icon } @@ -167,7 +167,7 @@ fun getIconId(demo: String): Int? { fun getSettingHeading(demo: String): Int? { var settingsString: Int? = null when (demo) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { settingsString = R.string.barcode_settings } @@ -197,6 +197,10 @@ fun getDemoTitle(demo: String): Int? { settingsString = R.string.barcode_demo } + UsecaseState.BarcodeMap.value -> { + settingsString = R.string.barcode_map_demo + } + UsecaseState.OCR.value -> { settingsString = R.string.ocr_demo } @@ -245,7 +249,8 @@ fun getSettingDescription(demo: String, setting: Int, value: Int): Int? { R.string.resolution -> { when (demo) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, + UsecaseState.BarcodeMap.value -> { when (value) { 0 -> { descString = R.string.resolution_1mp_desc_bc @@ -295,7 +300,8 @@ fun getSettingDescription(demo: String, setting: Int, value: Int): Int? { R.string.model_input_size -> { when (demo) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, + UsecaseState.BarcodeMap.value -> { when (value) { 0 -> { descString = R.string.model_input_size_640_bc diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt index b0df43a..da15fc9 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt @@ -32,6 +32,7 @@ sealed class Screen(val route: String) { object ProductsCapture : Screen("products_capture_screen") object OCRBarcodeCapture : Screen("ocrbarcode_capture_screen") object OCRBarcodeResults : Screen("ocrbarcode_results_screen") + object BarcodeMapResults : Screen("barcode_map_results_screen") object SingleResult : Screen("single_result_screen") /** @@ -114,6 +115,15 @@ fun NavigationStack( context = context ) } + composable(route = Screen.BarcodeMapResults.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.BarcodeMapResults) + BarcodeMapResultScreen( + viewModel, + navController = navController, + innerPadding, + context = context + ) + } composable(route = Screen.SingleResult.route + "?text={text}&bbox={bbox}&isBarcode={isBarcode}") { backStackEntry -> viewModel.updateActiveScreenData(activeScreen = Screen.SingleResult) val text = backStackEntry.arguments?.getString("text") ?: "" diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt index c99ce10..6eeafe7 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt @@ -166,6 +166,19 @@ class AIDataCaptureDemoViewModel( barcodeAnalyzer?.initialize() } + UsecaseState.BarcodeMap.value -> { + barcodeAnalyzer = BarcodeAnalyzer( + uiState = uiState, + viewModel = this@AIDataCaptureDemoViewModel + ) + barcodeAnalyzer?.initialize() + _uiState.update { currentState -> + currentState.copy( + isCaptureOrLiveEnabled = 0 // Default to Capture for Barcode Map + ) + } + } + UsecaseState.Retail.value -> { retailShelfAnalyzer = RetailShelfAnalyzer( uiState = uiState, @@ -217,7 +230,7 @@ class AIDataCaptureDemoViewModel( */ fun deinitModel() { when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { barcodeAnalyzer?.deinitialize() barcodeAnalyzer = null } @@ -331,6 +344,7 @@ class AIDataCaptureDemoViewModel( // Bind an additional Capture Use Case only for Product Recognition UsecaseState camera = if ((uiState.value.usecaseSelected == UsecaseState.Product.value) || + (uiState.value.usecaseSelected == UsecaseState.BarcodeMap.value) || ((uiState.value.usecaseSelected == UsecaseState.OCRBarcodeFind.value) && (uiState.value.isCaptureOrLiveEnabled == 0))){ // HIGH-RES CAPTURE CASE imageCaptureResolutionSelector = ResolutionSelector.Builder() @@ -565,7 +579,7 @@ class AIDataCaptureDemoViewModel( */ fun updateSelectedProcessor(index: Int) { val updatedSelectedProcessorIndex = when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { val currentProcessorSelectedIndex = _uiState.value.barcodeSettings.commonSettings currentProcessorSelectedIndex.copy(processorSelectedIndex = index) } @@ -597,7 +611,7 @@ class AIDataCaptureDemoViewModel( } } when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> _uiState.value.barcodeSettings.commonSettings = + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> _uiState.value.barcodeSettings.commonSettings = updatedSelectedProcessorIndex as CommonSettings UsecaseState.Retail.value -> _uiState.value.retailShelfSettings.commonSettings = @@ -632,7 +646,7 @@ class AIDataCaptureDemoViewModel( } val updatedInputSize = when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { val currentInputSizeSelected = _uiState.value.barcodeSettings.commonSettings currentInputSizeSelected.copy(inputSizeSelected = dimension) } @@ -669,7 +683,7 @@ class AIDataCaptureDemoViewModel( } } when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> _uiState.value.barcodeSettings.commonSettings = + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> _uiState.value.barcodeSettings.commonSettings = updatedInputSize as CommonSettings UsecaseState.Retail.value -> _uiState.value.retailShelfSettings.commonSettings = @@ -688,7 +702,7 @@ class AIDataCaptureDemoViewModel( fun updateSelectedResolution(index: Int) { val updatedResolution = when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { val currentResolutionSelectedIndex = _uiState.value.barcodeSettings.commonSettings currentResolutionSelectedIndex.copy(resolutionSelectedIndex = index) } @@ -720,7 +734,7 @@ class AIDataCaptureDemoViewModel( } } when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> _uiState.value.barcodeSettings.commonSettings = + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> _uiState.value.barcodeSettings.commonSettings = updatedResolution as CommonSettings UsecaseState.Retail.value -> _uiState.value.retailShelfSettings.commonSettings = @@ -739,7 +753,7 @@ class AIDataCaptureDemoViewModel( fun getSelectedResolution(): Int? { val currentResolutionSelectedIndex = when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { _uiState.value.barcodeSettings.commonSettings.resolutionSelectedIndex } @@ -768,7 +782,7 @@ class AIDataCaptureDemoViewModel( fun getProcessorSelectedIndex(): Int? { val currentProcessorSelectedIndex = when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { _uiState.value.barcodeSettings.commonSettings.processorSelectedIndex } @@ -797,7 +811,7 @@ class AIDataCaptureDemoViewModel( fun getInputSizeSelected(): Int? { val currentInputSizeSelected = when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { _uiState.value.barcodeSettings.commonSettings.inputSizeSelected } @@ -1624,7 +1638,7 @@ class AIDataCaptureDemoViewModel( fun saveSettings() { when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { FileUtils.saveBarcodeSettings(uiState.value.barcodeSettings) } @@ -1652,7 +1666,7 @@ class AIDataCaptureDemoViewModel( fun restoreDefaultSettings() { when (_uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { _uiState.value.barcodeSettings = BarcodeSettings() } @@ -1878,6 +1892,15 @@ class AIDataCaptureDemoViewModel( navController.navigateUp() } + fun saveBarcodeLayout() { + if (uiState.value.barcodeResults.isNotEmpty()) { + FileUtils.saveBarcodeResultsToFile(uiState.value.barcodeResults) + toast("Barcode layout saved successfully") + } else { + toast("No barcode results to save") + } + } + fun toast(toastString: String) { Toast.makeText(context, toastString, Toast.LENGTH_LONG).show() } @@ -1892,7 +1915,7 @@ class AIDataCaptureDemoViewModel( fun stopPreviewAnalysis() { when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { } @@ -1916,7 +1939,7 @@ class AIDataCaptureDemoViewModel( fun startPreviewAnalysis() { when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { } @@ -1944,6 +1967,10 @@ class AIDataCaptureDemoViewModel( } + UsecaseState.BarcodeMap.value -> { + barcodeAnalyzer!!.executeHighRes(highResBitmap) + } + UsecaseState.Retail.value -> { } diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/res/values/strings.xml b/AISuite_Demos/AIDataCaptureDemo/app/src/main/res/values/strings.xml index 3c6ef82..f5b4c2d 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/res/values/strings.xml +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/res/values/strings.xml @@ -11,6 +11,8 @@ Technology Demos Barcode Recognizer Detect and decode barcodes + Barcode Map + Detect and map barcodes with relative positions and labels Text/OCR Recognizer Detect and decode text with advanced settings Product & Shelf Recognizer diff --git a/AISuite_Demos/AIDataCaptureDemo/gradlew b/AISuite_Demos/AIDataCaptureDemo/gradlew old mode 100644 new mode 100755 diff --git a/AISuite_Demos/AI_Barcode_Finder/app/src/main/java/com/zebra/ai/barcodefinder/MainActivity.kt b/AISuite_Demos/AI_Barcode_Finder/app/src/main/java/com/zebra/ai/barcodefinder/MainActivity.kt index 537e95d..45da75c 100644 --- a/AISuite_Demos/AI_Barcode_Finder/app/src/main/java/com/zebra/ai/barcodefinder/MainActivity.kt +++ b/AISuite_Demos/AI_Barcode_Finder/app/src/main/java/com/zebra/ai/barcodefinder/MainActivity.kt @@ -21,6 +21,12 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val versionName = try { + packageManager.getPackageInfo(packageName, 0).versionName + } catch (e: Exception) { + "unknown" + } + Log.d(TAG, "MainActivity onCreate - AI Barcode Finder v$versionName") // Configure status bar color to match app title bar WindowCompat.setDecorFitsSystemWindows(window, true) diff --git a/AISuite_Demos/AI_Barcode_Finder/app/src/main/java/com/zebra/ai/barcodefinder/application/presentation/ui/compose/screens/homescreen/HomeScreen.kt b/AISuite_Demos/AI_Barcode_Finder/app/src/main/java/com/zebra/ai/barcodefinder/application/presentation/ui/compose/screens/homescreen/HomeScreen.kt index b671d25..4d9fce6 100644 --- a/AISuite_Demos/AI_Barcode_Finder/app/src/main/java/com/zebra/ai/barcodefinder/application/presentation/ui/compose/screens/homescreen/HomeScreen.kt +++ b/AISuite_Demos/AI_Barcode_Finder/app/src/main/java/com/zebra/ai/barcodefinder/application/presentation/ui/compose/screens/homescreen/HomeScreen.kt @@ -9,6 +9,11 @@ import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -28,6 +33,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu @@ -36,6 +43,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults @@ -74,6 +82,8 @@ import com.zebra.ai.barcodefinder.application.presentation.ui.theme.AppTextStyle import com.zebra.ai.barcodefinder.application.presentation.ui.theme.borderPrimaryMain import com.zebra.ai.barcodefinder.application.presentation.ui.theme.disabledMain import com.zebra.ai.barcodefinder.application.presentation.ui.theme.headerBackgroundColor +import com.zebra.ai.barcodefinder.application.presentation.ui.theme.iconGreen +import com.zebra.ai.barcodefinder.application.presentation.ui.theme.iconRed import com.zebra.ai.barcodefinder.application.presentation.viewmodel.HomeViewModel /** @@ -118,6 +128,18 @@ fun HomeScreen( val context = LocalContext.current var cameraPermissionDenied by remember { mutableStateOf(false) } + // Pulse animation for the Start Scan button when ready + val infiniteTransition = rememberInfiniteTransition(label = "Pulse") + val pulseColor by infiniteTransition.animateColor( + initialValue = borderPrimaryMain, + targetValue = borderPrimaryMain.copy(alpha = 0.7f), + animationSpec = infiniteRepeatable( + animation = tween(1200, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "PulseColor" + ) + // The launcher stays in the Composable, as it's part of the UI layer. val permissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() @@ -156,11 +178,44 @@ fun HomeScreen( Column(modifier = Modifier.fillMaxSize()) { TopAppBar( title = { - ZebraText( - textValue = stringResource(id = R.string.home_screen_content_app_name), - style = AppTextStyles.TitleTextLight, - textColor = AppColors.TextWhite - ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + ZebraText( + textValue = stringResource(id = R.string.home_screen_content_app_name), + style = AppTextStyles.TitleTextLight, + textColor = AppColors.TextWhite + ) + // SDK Status Indicator + Surface( + shape = RoundedCornerShape(AppDimensions.dimension_12dp), + color = if (entityTrackerInitState.isInitialized) iconGreen.copy(alpha = 0.2f) else iconRed.copy(alpha = 0.2f), + modifier = Modifier.padding(end = AppDimensions.dimension_16dp) + ) { + Row( + modifier = Modifier.padding(horizontal = AppDimensions.dimension_8dp, vertical = AppDimensions.dimension_2dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(AppDimensions.dimension_8dp) + .background( + color = if (entityTrackerInitState.isInitialized) iconGreen else iconRed, + shape = CircleShape + ) + ) + Spacer(modifier = Modifier.width(AppDimensions.dimension_4dp)) + Text( + text = if (entityTrackerInitState.isInitialized) "READY" else "INIT", + style = MaterialTheme.typography.labelSmall, + color = AppColors.TextWhite, + fontWeight = FontWeight.Bold + ) + } + } + } }, navigationIcon = { IconButton( @@ -200,102 +255,103 @@ fun HomeScreen( Spacer(modifier = Modifier.height(AppDimensions.dimension_40dp)) } Spacer(modifier = Modifier.height(AppDimensions.dimension_12dp)) - Column( + // Settings Summary Card + Surface( modifier = Modifier .fillMaxWidth() - .padding(top = AppDimensions.dimension_16dp), - verticalArrangement = Arrangement.spacedBy(AppDimensions.dimension_8dp) + .padding(horizontal = AppDimensions.dimension_16dp), + shape = RoundedCornerShape(AppDimensions.dimension_8dp), + color = AppColors.TextWhite, + shadowElevation = AppDimensions.dimension_2dp, + border = androidx.compose.foundation.BorderStroke(1.dp, AppColors.Divider.copy(alpha = 0.1f)) ) { - ZebraText( - textValue = stringResource(id = R.string.home_screen_content_settings_header), - fontSize = AppDimensions.dialogTextFontSizeMedium, - fontWeight = FontWeight.Bold, - textColor = AppColors.TextBlack, - textAlign = TextAlign.Start, + Column( modifier = Modifier .fillMaxWidth() - .semantics { contentDescription = "HomeScreenText" } - .padding(start = AppDimensions.dimension_16dp) - ) -// Spacer(modifier = Modifier.height(AppDimensions.dimension_2dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = AppDimensions.dimension_16dp), - verticalAlignment = Alignment.Top + .padding(AppDimensions.dimension_16dp), + verticalArrangement = Arrangement.spacedBy(AppDimensions.dimension_8dp) ) { ZebraText( - textValue = stringResource(id = R.string.home_screen_content_bullet_point), + textValue = stringResource(id = R.string.home_screen_content_settings_header), fontSize = AppDimensions.dialogTextFontSizeMedium, + fontWeight = FontWeight.Bold, textColor = AppColors.TextBlack, - fontWeight = FontWeight.Bold + textAlign = TextAlign.Start, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "HomeScreenText" } ) - Column { - Text( // Keep Material Text for buildAnnotatedString - text = buildAnnotatedString { - append(stringResource(id = R.string.home_screen_content_model_input)) - withStyle(style = SpanStyle(fontWeight = FontWeight.Light)) { - append(stringResource(id = settings.modelInput.homeDisplayNameResId)) - } - }, + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top + ) { + ZebraText( + textValue = stringResource(id = R.string.home_screen_content_bullet_point), fontSize = AppDimensions.dialogTextFontSizeMedium, - color = AppColors.TextBlack, + textColor = AppColors.TextBlack, fontWeight = FontWeight.Bold ) + Column { + Text( // Keep Material Text for buildAnnotatedString + text = buildAnnotatedString { + append(stringResource(id = R.string.home_screen_content_model_input)) + withStyle(style = SpanStyle(fontWeight = FontWeight.Light)) { + append(stringResource(id = settings.modelInput.homeDisplayNameResId)) + } + }, + fontSize = AppDimensions.dialogTextFontSizeMedium, + color = AppColors.TextBlack, + fontWeight = FontWeight.Bold + ) + } } - } -// Spacer(modifier = Modifier.height(AppDimensions.dimension_2dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = AppDimensions.dimension_16dp), - verticalAlignment = Alignment.Top - ) { - ZebraText( - textValue = stringResource(id = R.string.home_screen_content_bullet_point), - fontSize = AppDimensions.dialogTextFontSizeMedium, - textColor = AppColors.TextBlack, - fontWeight = FontWeight.Bold - ) - Column { - Text( // Keep Material Text for buildAnnotatedString - text = buildAnnotatedString { - append(stringResource(id = R.string.home_screen_content_resolution)) - withStyle(style = SpanStyle(fontWeight = FontWeight.Light)) { - append(stringResource(id = settings.resolution.displayNameResId)) - } - }, + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top + ) { + ZebraText( + textValue = stringResource(id = R.string.home_screen_content_bullet_point), fontSize = AppDimensions.dialogTextFontSizeMedium, - color = AppColors.TextBlack, + textColor = AppColors.TextBlack, fontWeight = FontWeight.Bold ) + Column { + Text( // Keep Material Text for buildAnnotatedString + text = buildAnnotatedString { + append(stringResource(id = R.string.home_screen_content_resolution)) + withStyle(style = SpanStyle(fontWeight = FontWeight.Light)) { + append(stringResource(id = settings.resolution.displayNameResId)) + } + }, + fontSize = AppDimensions.dialogTextFontSizeMedium, + color = AppColors.TextBlack, + fontWeight = FontWeight.Bold + ) + } } - } -// Spacer(modifier = Modifier.height(AppDimensions.dimension_2dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = AppDimensions.dimension_16dp), - verticalAlignment = Alignment.Top - ) { - ZebraText( - textValue = stringResource(id = R.string.home_screen_content_bullet_point), - fontSize = AppDimensions.dialogTextFontSizeMedium, - textColor = AppColors.TextBlack, - fontWeight = FontWeight.Bold - ) - Column { - Text( // Keep Material Text for buildAnnotatedString - text = buildAnnotatedString { - append(stringResource(id = R.string.home_screen_content_processor_type)) - withStyle(style = SpanStyle(fontWeight = FontWeight.Light)) { - append(stringResource(id = settings.processorType.displayNameResId)) - } - }, + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top + ) { + ZebraText( + textValue = stringResource(id = R.string.home_screen_content_bullet_point), fontSize = AppDimensions.dialogTextFontSizeMedium, - color = AppColors.TextBlack, + textColor = AppColors.TextBlack, fontWeight = FontWeight.Bold ) + Column { + Text( // Keep Material Text for buildAnnotatedString + text = buildAnnotatedString { + append(stringResource(id = R.string.home_screen_content_processor_type)) + withStyle(style = SpanStyle(fontWeight = FontWeight.Light)) { + append(stringResource(id = settings.processorType.displayNameResId)) + } + }, + fontSize = AppDimensions.dialogTextFontSizeMedium, + color = AppColors.TextBlack, + fontWeight = FontWeight.Bold + ) + } } } } @@ -306,6 +362,7 @@ fun HomeScreen( onClick = { homeViewModel.resetToDefaultSettings() homeViewModel.applySettingsToSDK() + android.widget.Toast.makeText(appContext, "Settings restored to default", android.widget.Toast.LENGTH_SHORT).show() }, modifier = Modifier.fillMaxWidth(), enabled = entityTrackerInitState.isInitialized, @@ -334,7 +391,7 @@ fun HomeScreen( }, enabled = entityTrackerInitState.isInitialized, shapes = RoundedCornerShape(AppDimensions.dimension_4dp), - backgroundColor = if (entityTrackerInitState.isInitialized) borderPrimaryMain else disabledMain, + backgroundColor = if (entityTrackerInitState.isInitialized) pulseColor else disabledMain, textColor = AppColors.TextWhite, // leadingIcon = if (!entityTrackerInitState.isInitialized) { // { From 7a8aaaae680b5e43489aa7c718c1a95f7ef46b02 Mon Sep 17 00:00:00 2001 From: MichelleWu09 <156372216+MichelleWu09@users.noreply.github.com> Date: Fri, 29 May 2026 12:03:12 -0400 Subject: [PATCH 02/47] update item picking screen --- .DS_Store | Bin 6148 -> 6148 bytes AISuite_Demos/.DS_Store | Bin 6148 -> 6148 bytes .../data/AIDataCaptureDemoUiState.kt | 1 + .../ui/view/BarcodeMapPickingScreen.kt | 295 ++++++++++++++++++ .../ui/view/BarcodeMapResultScreen.kt | 247 ++++++++------- .../ui/view/DemoSettingsScreen.kt | 10 +- .../ui/view/GlobalConstants.kt | 6 +- .../ui/view/NavigationStack.kt | 11 + .../viewmodel/AIDataCaptureDemoViewModel.kt | 11 + .../app/src/main/res/values/strings.xml | 1 + AISuite_QuickStart/.DS_Store | Bin 0 -> 6148 bytes 11 files changed, 474 insertions(+), 108 deletions(-) create mode 100644 AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt create mode 100644 AISuite_QuickStart/.DS_Store diff --git a/.DS_Store b/.DS_Store index 531cb231781182ada628c328492bb3b2f563d06e..92188f9633ca3c094fa9c32ce51799cec18ef726 100644 GIT binary patch delta 249 zcmZoMXffEJ$`YrxJc)sUfrUYjA)O(Up(Hoo#U&{xA0)={jH^vN=E-qKRQVLV@&y@& z!O8i#1wcIvOgalDH?v%3&Uf{jyq8sOaye@ubI1XP$?=JDWArx1}If1UnB$h6%*6ApQh2{t0$I#t9P> L4K}lL{N)D#h*C(( delta 249 zcmZoMXffEJ$`Yq4xs-u{frUYjA)O(Up(Hoo#U&{xKM5$t@$xj2Q|6=Nj;Qh}c;yQ+ z41<&Na|?ia7?{)=CO5NOX3h&@nY@=(ZgM$mA#?BnhRN}48Ui`#2z3k*N7R9WT9bX* zxY%r}1n*=#njFh!hG1$5AREgN%fK*+U1jojHfyGP3qh<1b{-DKzQh9=K*xgk6VUi4 R*!dU>CMFteX6N|J4* = mutableListOf(), val ocrResults: List = listOf(), var barcodeResults: List = listOf(), + var selectedToteId: String? = null, // Choices var isBarcodeModelEnabled: Boolean = true, diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt new file mode 100644 index 0000000..af7e05a --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt @@ -0,0 +1,295 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.ui.view + +import android.content.Context +import android.util.Size +import android.view.WindowManager +import android.view.WindowMetrics +import androidx.activity.compose.BackHandler +import androidx.camera.view.PreviewView +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat.getSystemService +import androidx.lifecycle.Lifecycle +import androidx.navigation.NavController +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import kotlin.math.min +import androidx.compose.ui.graphics.drawscope.DrawScope +import com.zebra.aidatacapturedemo.data.ResultData +import kotlin.math.abs + +@Composable +fun BarcodeMapPickingScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + context: Context, + activityInnerPadding: PaddingValues, + activityLifecycle: Lifecycle +) { + val uiState by viewModel.uiState.collectAsState() + val lifecycleOwner = LocalLifecycleOwner.current + val previewView = remember { PreviewView(context) } + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + viewModel.updateAppBarTitle("Item Picking") + + // Ensure we are in Live mode for item scanning + LaunchedEffect(Unit) { + viewModel.updateCaptureOrLiveEnabled(1) // Set to Live + } + + // Setup camera to scan items + LaunchedEffect(lifecycleOwner) { + viewModel.setupCameraController( + previewView = previewView, + analysisUseCaseCameraResolution = Size(1280, 720), + lifecycleOwner = lifecycleOwner, + activityLifecycle = activityLifecycle + ) + } + + // Logic to "decide which tote": + // In this demo, if a barcode is detected, we match it to a tote. + LaunchedEffect(uiState.barcodeResults) { + if (uiState.barcodeResults.isNotEmpty()) { + val detectedText = uiState.barcodeResults.first().text + // In a real app, we'd lookup which tote 'detectedText' belongs to. + // For this demo, let's just use the first detected one as the target + viewModel.updateSelectedToteId(detectedText) + } + } + + Box(modifier = Modifier.fillMaxSize()) { + // 1. Full screen Abstract Map (The "Digital Twin") + AbstractMapLayer(uiState) + + // 2. Small Camera Preview in the corner to scan the item + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(bottom = activityInnerPadding.calculateBottomPadding() + 16.dp, end = 16.dp) + .size(160.dp, 120.dp) + .background(Color.Black, RoundedCornerShape(12.dp)) + .padding(2.dp) + ) { + AndroidView( + factory = { previewView }, + modifier = Modifier.fillMaxSize() + ) + } + + // 3. Guidance Overlay + if (uiState.selectedToteId != null) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 100.dp), + contentAlignment = Alignment.TopCenter + ) { + Text( + text = "Item Identfied! Place in Tote: ${uiState.selectedToteId}", + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ), + modifier = Modifier + .background(Color(0xFF006D39), RoundedCornerShape(8.dp)) + .padding(16.dp) + ) + } + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 100.dp), + contentAlignment = Alignment.TopCenter + ) { + Text( + text = "Scan Item Barcode", + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ), + modifier = Modifier + .background(Color.White.copy(alpha = 0.8f), RoundedCornerShape(8.dp)) + .padding(16.dp) + ) + } + } + } +} + +@Composable +private fun AbstractMapLayer(uiState: AIDataCaptureDemoUiState) { + val capturedBitmap = uiState.captureBitmap ?: return + + val displayMetrics = LocalContext.current.resources.displayMetrics + val displayMetricsDensity = displayMetrics.density + val windowManager = getSystemService(LocalContext.current, WindowManager::class.java) + val windowMetrics: WindowMetrics = windowManager!!.currentWindowMetrics + val displayTotalWidthInPx = windowMetrics.bounds.width() + val displayTotalHeightInPx = windowMetrics.bounds.height() + + // Simplified scaling logic for the abstract map + val scaler = min( + displayTotalWidthInPx.toFloat() / capturedBitmap.width.toFloat(), + displayTotalHeightInPx.toFloat() / capturedBitmap.height.toFloat() + ) + val gapX = (displayTotalWidthInPx - (scaler * capturedBitmap.width.toFloat())) / 2f + val gapY = (displayTotalHeightInPx - (scaler * capturedBitmap.height.toFloat())) / 2f + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFFF0F2F5)) + ) { + DrawAbstractBarcodeMapLayer( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + } +} + +@Composable +private fun DrawAbstractBarcodeMapLayer( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float +) { + val barcodeResults = uiState.barcodeResults + if (barcodeResults.isEmpty()) return + + // Grouping logic for rows (Reusing logic from Result screen) + val sortedByY = barcodeResults.sortedBy { it.boundingBox.centerY() } + val rows = mutableListOf>() + + if (sortedByY.isNotEmpty()) { + var currentRow = mutableListOf() + currentRow.add(sortedByY[0]) + rows.add(currentRow) + + for (i in 1 until sortedByY.size) { + val prev = sortedByY[i - 1] + val curr = sortedByY[i] + if (abs(curr.boundingBox.centerY() - prev.boundingBox.centerY()) < (prev.boundingBox.height() * 0.6)) { + currentRow.add(curr) + } else { + currentRow = mutableListOf() + currentRow.add(curr) + rows.add(currentRow) + } + } + } + + Canvas(modifier = Modifier.fillMaxSize()) { + rows.forEach { row -> + val sortedRow = row.sortedBy { it.boundingBox.left } + val avgHeight = sortedRow.map { it.boundingBox.height() }.average().toFloat() + val avgCenterY = sortedRow.map { it.boundingBox.centerY() }.average().toFloat() + + var currentLeftX = -1f + + sortedRow.forEach { barcode -> + val bBoxWidth = barcode.boundingBox.width().toFloat() + var left = barcode.boundingBox.left.toFloat() + + if (currentLeftX != -1f) { + if (abs(left - currentLeftX) < bBoxWidth * 0.4) { + left = currentLeftX + } + } + + val scaledLeft = (scaler * left) + gapX + val scaledTop = (scaler * (avgCenterY - avgHeight/2)) + gapY + val scaledWidth = (scaler * bBoxWidth) + val scaledHeight = (scaler * avgHeight) + + // Highlight if it's the selected tote + val isTarget = uiState.selectedToteId == barcode.text + + drawAbstractPickingUnit( + id = barcode.text, + left = scaledLeft, + top = scaledTop, + width = scaledWidth, + height = scaledHeight, + density = displayMetricsDensity, + isTarget = isTarget + ) + + currentLeftX = left + bBoxWidth + } + } + } +} + +private fun DrawScope.drawAbstractPickingUnit( + id: String, + left: Float, + top: Float, + width: Float, + height: Float, + density: Float, + isTarget: Boolean +) { + val themeColor = if (isTarget) Color(0xFFFFCC00) else Color(0xFF00FF00) // Gold for target, Green for others + val rectSize = androidx.compose.ui.geometry.Size(width, height) + val topLeft = Offset(left, top) + + drawRect( + color = themeColor.copy(alpha = if (isTarget) 0.8f else 0.2f), + topLeft = topLeft, + size = rectSize + ) + + drawRect( + color = if (isTarget) Color.Red else themeColor, + topLeft = topLeft, + size = rectSize, + style = Stroke(width = (if (isTarget) 4f else 2f) * density) + ) + + val paint = android.graphics.Paint().apply { + this.color = if (isTarget) android.graphics.Color.WHITE else android.graphics.Color.BLACK + this.textSize = (if (isTarget) 12f else 9f) * density + this.textAlign = android.graphics.Paint.Align.CENTER + this.isAntiAlias = true + this.isFakeBoldText = true + } + + val textX = left + width / 2 + val textY = top + height / 2 - (paint.fontMetrics.ascent + paint.fontMetrics.descent) / 2 + + if (width > 25 * density) { + val displayId = if (id.length > 7) id.take(5) + ".." else id + drawContext.canvas.nativeCanvas.drawText(displayId, textX, textY, paint) + } +} diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt index 6829764..78e709a 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -34,7 +33,6 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.nativeCanvas -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -45,10 +43,13 @@ import androidx.navigation.NavController import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel import kotlin.math.min +import androidx.compose.ui.graphics.drawscope.DrawScope +import com.zebra.aidatacapturedemo.data.ResultData +import kotlin.math.abs /** - * BarcodeMapResultScreen is a Composable function that displays the relative positions - * of the detected barcodes and their IDs on a clean background. + * BarcodeMapResultScreen is a Composable function that displays an abstract + * geometrical layout of detected barcodes on a clean background. */ @Composable fun BarcodeMapResultScreen( @@ -62,7 +63,7 @@ fun BarcodeMapResultScreen( BackHandler(enabled = true) { viewModel.handleBackButton(navController) } - viewModel.updateAppBarTitle("Barcode Map") + viewModel.updateAppBarTitle("Barcode Layout Map") val capturedBitmap = uiState.captureBitmap if (capturedBitmap == null) { @@ -99,35 +100,29 @@ fun BarcodeMapResultScreen( val availableHeightInPx = displayTotalHeightInPx.toFloat() - displayStatusBarHeightInPx - displayNavigationBarHeightInPx - // The following computed values are used for drawing Bbox overlay + // The following computed values are used for drawing val scaler = min( displayTotalWidthInPx.toFloat() / capturedBitmap.width.toFloat(), availableHeightInPx / capturedBitmap.height.toFloat() ) - val scaledWidth = scaler * capturedBitmap.width.toFloat() - val scaledHeight = scaler * capturedBitmap.height.toFloat() - val gapX = (displayTotalWidthInPx - scaledWidth) / 2f - val gapY = (availableHeightInPx - scaledHeight) / 2f + val gapX = (displayTotalWidthInPx - (scaler * capturedBitmap.width.toFloat())) / 2f + val gapY = (availableHeightInPx - (scaler * capturedBitmap.height.toFloat())) / 2f - - Box( // Bottom layer + Box( modifier = Modifier .fillMaxSize() .padding( top = displayStatusBarHeightInDp, bottom = displayNavigationBarHeightInDp ) - .background(color = Color(0xFFF0F0F0)) // Clean light gray background + .background(color = Color(0xFFF0F2F5)) // Clean modern background ) { - - // MAP CANVAS + // ABSTRACT MAP CANVAS Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - // We don't draw the captured image anymore as per user request - - DrawBarcodeMapOnCanvas( + DrawAbstractBarcodeMap( uiState = uiState, scaler = scaler, gapX = gapX, @@ -140,44 +135,52 @@ fun BarcodeMapResultScreen( Box( modifier = Modifier .fillMaxWidth() - .padding(top = 16.dp), + .padding(top = 24.dp), contentAlignment = Alignment.TopCenter ) { - Text( - text = "Detected ${uiState.barcodeResults.size} Barcodes", - style = TextStyle( - fontSize = 18.sp, - fontWeight = FontWeight.Bold, - color = Color.Black - ), - modifier = Modifier - .background(Color.White.copy(alpha = 0.8f), RoundedCornerShape(8.dp)) - .padding(horizontal = 16.dp, vertical = 8.dp) - ) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "Barcode Layout Map", + style = TextStyle( + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1C1E) + ) + ) + Text( + text = "${uiState.barcodeResults.size} barcodes mapped", + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFF44474E) + ) + ) + } } // SAVE BUTTON Box( modifier = Modifier .fillMaxSize() - .padding(bottom = 24.dp), + .padding(bottom = 32.dp), contentAlignment = Alignment.BottomCenter ) { Button( onClick = { viewModel.saveBarcodeLayout() + navController.navigate(Screen.BarcodeMapPicking.route) }, modifier = Modifier - .fillMaxWidth(0.7f) - .height(56.dp), - shape = RoundedCornerShape(28.dp), - colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF006400)) + .fillMaxWidth(0.6f) + .height(54.dp), + shape = RoundedCornerShape(27.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF006D39)) // Dark green ) { Text( - text = "Save Layout", + text = "Save Barcode Map", style = TextStyle( - fontSize = 18.sp, - fontWeight = FontWeight.Bold, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, color = Color.White ) ) @@ -188,90 +191,124 @@ fun BarcodeMapResultScreen( } @Composable -private fun DrawBarcodeMapOnCanvas( +private fun DrawAbstractBarcodeMap( uiState: AIDataCaptureDemoUiState, scaler: Float, gapX: Float, gapY: Float, displayMetricsDensity: Float ) { - Canvas( - modifier = Modifier - .fillMaxSize() - ) { - uiState.barcodeResults.forEachIndexed { index, barcodeData -> - barcodeData?.let { + val barcodeResults = uiState.barcodeResults + if (barcodeResults.isEmpty()) return + + // Grouping logic for rows + val sortedByY = barcodeResults.sortedBy { it.boundingBox.centerY() } + val rows = mutableListOf>() + + if (sortedByY.isNotEmpty()) { + var currentRow = mutableListOf() + currentRow.add(sortedByY[0]) + rows.add(currentRow) + + for (i in 1 until sortedByY.size) { + val prev = sortedByY[i - 1] + val curr = sortedByY[i] + + // Overlap threshold for same row + if (abs(curr.boundingBox.centerY() - prev.boundingBox.centerY()) < (prev.boundingBox.height() * 0.6)) { + currentRow.add(curr) + } else { + currentRow = mutableListOf() + currentRow.add(curr) + rows.add(currentRow) + } + } + } - val bBoxTop = barcodeData.boundingBox.top.toFloat() - val bBoxLeft = barcodeData.boundingBox.left.toFloat() - val bBoxBottom = barcodeData.boundingBox.bottom.toFloat() - val bBoxRight = barcodeData.boundingBox.right.toFloat() + Canvas(modifier = Modifier.fillMaxSize()) { + rows.forEach { row -> + val sortedRow = row.sortedBy { it.boundingBox.left } + + // Normalize row metrics to average + val avgHeight = sortedRow.map { it.boundingBox.height() }.average().toFloat() + val avgCenterY = sortedRow.map { it.boundingBox.centerY() }.average().toFloat() - val scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX - val scaledBBoxTopInPx = (scaler * bBoxTop) + gapY - val scaledBBoxRightInPx = (scaler * bBoxRight) + gapX - val scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + var currentLeftX = -1f - // Define the size and position of the rectangle - val rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx - val rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx - val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + sortedRow.forEachIndexed { index, barcode -> + val bBoxWidth = barcode.boundingBox.width().toFloat() + var left = barcode.boundingBox.left.toFloat() + + // Snapping logic: if close to previous, snap to it + if (currentLeftX != -1f) { + if (abs(left - currentLeftX) < bBoxWidth * 0.4) { + left = currentLeftX + } + } - // 1. Draw Bounding Box - drawRect( - color = Color(0xFF00FF00), // zebra green - topLeft = topLeftOffset, - size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), - style = Stroke(width = (2f * displayMetricsDensity)) - ) + val scaledLeft = (scaler * left) + gapX + val scaledTop = (scaler * (avgCenterY - avgHeight/2)) + gapY + val scaledWidth = (scaler * bBoxWidth) + val scaledHeight = (scaler * avgHeight) - // 2. Draw semi-transparent overlay inside the box - drawRect( - color = Color(0x3300FF00), - topLeft = topLeftOffset, - size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight) + drawAbstractUnit( + id = barcode.text, + left = scaledLeft, + top = scaledTop, + width = scaledWidth, + height = scaledHeight, + density = displayMetricsDensity ) + + // Track where the next one should start if it snaps + currentLeftX = left + bBoxWidth + } + } + } +} - // 3. Prepare label text - val idText = if (barcodeData.text.isNotEmpty()) barcodeData.text else "N/A" - val labelText = "#${index + 1}: $idText" - - val paint = android.graphics.Paint().apply { - color = android.graphics.Color.WHITE - textSize = 14f * displayMetricsDensity - typeface = android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD) - isAntiAlias = true - } +private fun DrawScope.drawAbstractUnit( + id: String, + left: Float, + top: Float, + width: Float, + height: Float, + density: Float +) { + val themeColor = Color(0xFF00FF00) // Vibrant Green + val rectSize = androidx.compose.ui.geometry.Size(width, height) + val topLeft = Offset(left, top) - val textWidth = paint.measureText(labelText) - val fontMetrics = paint.fontMetrics - val textHeight = fontMetrics.descent - fontMetrics.ascent + // 1. Draw simple geometrical shape (Rectangle) + drawRect( + color = themeColor.copy(alpha = 0.2f), + topLeft = topLeft, + size = rectSize + ) - // Position label above the barcode - val padding = 4f * displayMetricsDensity - val labelX = scaledBBoxLeftInPx - var labelY = scaledBBoxTopInPx - textHeight - (2 * padding) + // 2. Draw sharp outline + drawRect( + color = themeColor, + topLeft = topLeft, + size = rectSize, + style = Stroke(width = 2f * density) + ) - // Ensure label is within screen bounds - if (labelY < 0) { - labelY = scaledBBoxBottomInPx + padding - } + // 3. Center-aligned ID text + val paint = android.graphics.Paint().apply { + this.color = android.graphics.Color.BLACK + this.textSize = 9f * density + this.textAlign = android.graphics.Paint.Align.CENTER + this.isAntiAlias = true + this.isFakeBoldText = true + } - // Draw label background - drawRect( - color = Color(0xFF006400), // Darker green - topLeft = Offset(labelX, labelY), - size = androidx.compose.ui.geometry.Size(textWidth + (2 * padding), textHeight + padding) - ) + val textX = left + width / 2 + val textY = top + height / 2 - (paint.fontMetrics.ascent + paint.fontMetrics.descent) / 2 - // Draw text - drawContext.canvas.nativeCanvas.drawText( - labelText, - labelX + padding, - labelY + textHeight - fontMetrics.descent, - paint - ) - } - } + // Only draw ID if it fits within the simplified shape + if (width > 25 * density) { + val displayId = if (id.length > 7) id.take(5) + ".." else id + drawContext.canvas.nativeCanvas.drawText(displayId, textX, textY, paint) } } diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt index 80fc35a..5cbc2cb 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt @@ -639,14 +639,20 @@ fun AddAboutInformation(viewModel: AIDataCaptureDemoViewModel) { ) } - UsecaseState.Barcode.value, - UsecaseState.BarcodeMap.value -> { + UsecaseState.Barcode.value -> { Pair( first = "Barcode Recognizer Version", second = BuildConfig.BarcodeLocalizer_Version ) } + UsecaseState.BarcodeMap.value -> { + Pair( + first = "Barcode Map Version", + second = BuildConfig.BarcodeLocalizer_Version + ) + } + UsecaseState.Product.value -> { Pair( first = "Product & Shelf Enrollment Version", diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt index 4b8fa62..b4bd8fb 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt @@ -167,10 +167,14 @@ fun getIconId(demo: String): Int? { fun getSettingHeading(demo: String): Int? { var settingsString: Int? = null when (demo) { - UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + UsecaseState.Barcode.value -> { settingsString = R.string.barcode_settings } + UsecaseState.BarcodeMap.value -> { + settingsString = R.string.barcode_map_settings + } + UsecaseState.OCR.value -> { settingsString = R.string.text_ocr_recognizer_settings } diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt index da15fc9..fbf46f6 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt @@ -33,6 +33,7 @@ sealed class Screen(val route: String) { object OCRBarcodeCapture : Screen("ocrbarcode_capture_screen") object OCRBarcodeResults : Screen("ocrbarcode_results_screen") object BarcodeMapResults : Screen("barcode_map_results_screen") + object BarcodeMapPicking : Screen("barcode_map_picking_screen") object SingleResult : Screen("single_result_screen") /** @@ -124,6 +125,16 @@ fun NavigationStack( context = context ) } + composable(route = Screen.BarcodeMapPicking.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.BarcodeMapPicking) + BarcodeMapPickingScreen( + viewModel = viewModel, + navController = navController, + context = context, + activityInnerPadding = activityInnerPadding, + activityLifecycle = activityLifecycle + ) + } composable(route = Screen.SingleResult.route + "?text={text}&bbox={bbox}&isBarcode={isBarcode}") { backStackEntry -> viewModel.updateActiveScreenData(activeScreen = Screen.SingleResult) val text = backStackEntry.arguments?.getString("text") ?: "" diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt index 6eeafe7..20dcf21 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt @@ -1887,6 +1887,8 @@ class AIDataCaptureDemoViewModel( updateSelectedFilterType(filterType = FilterType.NONE) } else if (currentScreen == Screen.BarcodeFindFilterHome) { updateSelectedFilterType(filterType = FilterType.NONE) + } else if (currentScreen == Screen.BarcodeMapPicking) { + updateSelectedToteId(null) } setZoom(1.0f) navController.navigateUp() @@ -2027,6 +2029,15 @@ class AIDataCaptureDemoViewModel( ) } } + + fun updateSelectedToteId(id: String?) { + _uiState.update { currentState -> + currentState.copy( + selectedToteId = id + ) + } + } + fun clearOcrBarcodeCaptureSession(){ updateOcrBarcodeCaptureSessionIndex(0) updateOcrBarcodeCaptureSessionCount(0) diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/res/values/strings.xml b/AISuite_Demos/AIDataCaptureDemo/app/src/main/res/values/strings.xml index f5b4c2d..3d4c85f 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/res/values/strings.xml +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/res/values/strings.xml @@ -40,6 +40,7 @@ Scan or Type Product SKU Barcode Recognizer Settings + Barcode Map Settings OCR & Barcode Settings Text/OCR Recognizer Settings Product & Shelf Recognizer Settings diff --git a/AISuite_QuickStart/.DS_Store b/AISuite_QuickStart/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2993e12fd8a95991bea9c0887fcfc6fcbc8d43bf GIT binary patch literal 6148 zcmeHKJ5Iwu5S>jT2!bM_3Y}XTL~1BZWC~g$QBXlJN(x&xNc27iCqUeQBT#SyM8y@j z0B?3Cv5jMiDne*Rntki}cy^yZyGulF{1A_b21L|{GIqAmtPu9IHl(AL9ia2~h$*4l zWHy@IWQ}Mu{6z-%*=^DR&FG%4sq*~NT!#B{*NjNBD4HhO6rTR$``O3*^Yvs^RQ!vm ze9mu+R&$43w2P7Cl*@aho{P8V(o?O!ceMMY^))Ze%DPQ&t=^@tx6wZPuVYlL`Kz_} z&xecJKVr%ew8hlM;rRPAF$RnQV_X8(z>)#}KLk+5P_Yp7zYaA1 z2mtKC>;-+lmjMQB0EUW%AS@6ksX$3}dc<&&4tt<+p<*E@>EwK6+(#=rJ)taiQZDe} z Date: Fri, 29 May 2026 12:08:07 -0400 Subject: [PATCH 03/47] Update .DS_Store --- AISuite_Demos/.DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/AISuite_Demos/.DS_Store b/AISuite_Demos/.DS_Store index 48984549e1fd3be9e4908c6365d70672b7d1f6bd..d3326d1345dcaab6464ceafb240c28a7a30abe72 100644 GIT binary patch delta 52 zcmZoMXfc@JFUrfnz`)4BAi%(o$WX+P%#hEJ!jQUIkYhPBBk$yoENYuKv4k>DY-rfb I&heKY04014I{*Lx delta 176 zcmZoMXfc@JFD%Hwz`)4BAi%(on3HZ8oSdIquvw5}C38JUf|Ws!A(J76Ar&DB6a{K1 z5kr;D&3AE0%E?axigM)tI9+k{@o`6V6)6N%6l6fG2kHab!1RG(av_V_W+RqR=FRLJ GfB6A3?kl Date: Tue, 2 Jun 2026 15:11:22 -0400 Subject: [PATCH 04/47] Update BarcodeMapPickingScreen.kt Deleted the live camera in the BarcodeMapPickingScreen --- .../ui/view/BarcodeMapPickingScreen.kt | 44 ++----------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt index af7e05a..b6097dd 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt @@ -3,11 +3,9 @@ package com.zebra.aidatacapturedemo.ui.view import android.content.Context -import android.util.Size import android.view.WindowManager import android.view.WindowMetrics import androidx.activity.compose.BackHandler -import androidx.camera.view.PreviewView import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -21,12 +19,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat.getSystemService import androidx.lifecycle.Lifecycle import androidx.navigation.NavController @@ -41,34 +37,17 @@ import kotlin.math.abs fun BarcodeMapPickingScreen( viewModel: AIDataCaptureDemoViewModel, navController: NavController, - context: Context, - activityInnerPadding: PaddingValues, - activityLifecycle: Lifecycle + @Suppress("UNUSED_PARAMETER") context: Context, + @Suppress("UNUSED_PARAMETER") activityInnerPadding: PaddingValues, + @Suppress("UNUSED_PARAMETER") activityLifecycle: Lifecycle ) { val uiState by viewModel.uiState.collectAsState() - val lifecycleOwner = LocalLifecycleOwner.current - val previewView = remember { PreviewView(context) } BackHandler(enabled = true) { viewModel.handleBackButton(navController) } viewModel.updateAppBarTitle("Item Picking") - // Ensure we are in Live mode for item scanning - LaunchedEffect(Unit) { - viewModel.updateCaptureOrLiveEnabled(1) // Set to Live - } - - // Setup camera to scan items - LaunchedEffect(lifecycleOwner) { - viewModel.setupCameraController( - previewView = previewView, - analysisUseCaseCameraResolution = Size(1280, 720), - lifecycleOwner = lifecycleOwner, - activityLifecycle = activityLifecycle - ) - } - // Logic to "decide which tote": // In this demo, if a barcode is detected, we match it to a tote. LaunchedEffect(uiState.barcodeResults) { @@ -84,22 +63,7 @@ fun BarcodeMapPickingScreen( // 1. Full screen Abstract Map (The "Digital Twin") AbstractMapLayer(uiState) - // 2. Small Camera Preview in the corner to scan the item - Box( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(bottom = activityInnerPadding.calculateBottomPadding() + 16.dp, end = 16.dp) - .size(160.dp, 120.dp) - .background(Color.Black, RoundedCornerShape(12.dp)) - .padding(2.dp) - ) { - AndroidView( - factory = { previewView }, - modifier = Modifier.fillMaxSize() - ) - } - - // 3. Guidance Overlay + // 2. Guidance Overlay if (uiState.selectedToteId != null) { Box( modifier = Modifier From a6eda734be19080554e66905db8d2062d826fb44 Mon Sep 17 00:00:00 2001 From: cachen-hi Date: Thu, 4 Jun 2026 10:54:16 -0400 Subject: [PATCH 05/47] Added customer information page and product page with product name, price, and barcode number --- .../data/AIDataCaptureDemoUiState.kt | 10 +- .../aidatacapturedemo/data/CustomerInfo.kt | 36 ++++ .../ui/view/BarcodeMapPickingScreen.kt | 10 +- .../ui/view/BarcodeMapResultScreen.kt | 2 +- .../ui/view/BarcodeScanPickingScreen.kt | 160 ++++++++++++++++++ .../ui/view/CameraPreviewScreen.kt | 32 ++++ .../ui/view/CustomerInformationScreen.kt | 152 +++++++++++++++++ .../ui/view/NavigationStack.kt | 18 ++ .../viewmodel/AIDataCaptureDemoViewModel.kt | 66 ++++++++ AISuite_QuickStart/.DS_Store | Bin 6148 -> 6148 bytes 10 files changed, 477 insertions(+), 9 deletions(-) create mode 100644 AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/CustomerInfo.kt create mode 100644 AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeScanPickingScreen.kt create mode 100644 AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt index 4c9c4a9..b3fb4a9 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt @@ -9,11 +9,6 @@ import com.zebra.aidatacapturedemo.ui.view.Screen /** * AIDataCaptureDemoUiState.kt is a data class that holds the UI state for the AI Data Capture Demo - * application. It includes various settings and results related to barcode scanning, OCR, - * Retail shelf recognition, and Product recognition. The state is updated based on user - * interactions and model outputs, allowing the UI to reactively display the current status of the - * application. This class is used to manage the state of the application and facilitate - * communication between the UI and the underlying models. */ val PROFILING = "Profiling" @@ -193,6 +188,11 @@ data class AIDataCaptureDemoUiState( val ocrResults: List = listOf(), var barcodeResults: List = listOf(), var selectedToteId: String? = null, + var allCustomers: List = listOf(), + var selectedCustomer: CustomerInfo? = null, + var pickingFeedback: String? = null, + var lastScannedProduct: ProductInfo? = null, + var targetTotes: List> = listOf(), // Tote ID to Quantity // Choices var isBarcodeModelEnabled: Boolean = true, diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/CustomerInfo.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/CustomerInfo.kt new file mode 100644 index 0000000..e392831 --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/CustomerInfo.kt @@ -0,0 +1,36 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.data + +import kotlin.random.Random + +data class ProductInfo( + val name: String, + val price: Double, + val barcode: String, + val quantity: Int = Random.nextInt(1, 10) +) + +data class CustomerInfo( + val id: String, + val products: List +) + +object CustomerDataGenerator { + private val availableProducts = listOf( + ProductInfo("Heidrun opbergbox met klapedeksel", 2.99, "2540068"), + ProductInfo("Opbergbox+klemdeksel A4", 2.49, "2543429"), + ProductInfo("Onderbedboxklemdeksel", 5.95, "2568528"), + ProductInfo("Opbergbox met klemdeksel", 7.49, "3205800") + ) + + fun generateCustomers(): List { + return listOf("A", "B", "C", "D", "E", "F").map { id -> + val numProducts = Random.nextInt(1, 4) + val selectedProducts = availableProducts.shuffled().take(numProducts).map { + it.copy(quantity = Random.nextInt(1, 6)) + } + CustomerInfo(id, selectedProducts) + } + } +} diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt index b6097dd..820e903 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt @@ -64,7 +64,8 @@ fun BarcodeMapPickingScreen( AbstractMapLayer(uiState) // 2. Guidance Overlay - if (uiState.selectedToteId != null) { + val feedback = uiState.pickingFeedback + if (feedback != null) { Box( modifier = Modifier .fillMaxWidth() @@ -72,14 +73,17 @@ fun BarcodeMapPickingScreen( contentAlignment = Alignment.TopCenter ) { Text( - text = "Item Identfied! Place in Tote: ${uiState.selectedToteId}", + text = feedback, style = TextStyle( fontSize = 18.sp, fontWeight = FontWeight.Bold, color = Color.White ), modifier = Modifier - .background(Color(0xFF006D39), RoundedCornerShape(8.dp)) + .background( + if (feedback.contains("incorrect")) Color.Red else Color(0xFF006D39), + RoundedCornerShape(8.dp) + ) .padding(16.dp) ) } diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt index 78e709a..579e15e 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt @@ -168,7 +168,7 @@ fun BarcodeMapResultScreen( Button( onClick = { viewModel.saveBarcodeLayout() - navController.navigate(Screen.BarcodeMapPicking.route) + navController.navigate(Screen.CustomerInformation.route) }, modifier = Modifier .fillMaxWidth(0.6f) diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeScanPickingScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeScanPickingScreen.kt new file mode 100644 index 0000000..9478971 --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeScanPickingScreen.kt @@ -0,0 +1,160 @@ +package com.zebra.aidatacapturedemo.ui.view + +import android.content.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.ProductInfo +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +@Composable +fun BarcodeScanPickingScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + @Suppress("UNUSED_PARAMETER") innerPadding: PaddingValues +) { + val uiState by viewModel.uiState.collectAsState() + val context = LocalContext.current + val focusRequester = remember { FocusRequester() } + var manualInput by remember { mutableStateOf("") } + + // Register BroadcastReceiver for DataWedge + DisposableEffect(Unit) { + val filter = IntentFilter("com.zebra.aidatacapturedemo.SCAN") + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val scanData = intent?.getStringExtra("com.symbol.datawedge.data_string") + if (scanData != null) { + viewModel.processHardwareScan(scanData) + } + } + } + context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED) + onDispose { + context.unregisterReceiver(receiver) + } + } + + // Auto-focus the manual input field to capture keyboard wedge scans + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFFF0F2F5)) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Ready to Scan", + style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color.Black), + modifier = Modifier.padding(top = 32.dp, bottom = 16.dp) + ) + + // Invisible or small TextField to capture keyboard wedge input + OutlinedTextField( + value = manualInput, + onValueChange = { + manualInput = it + if (it.endsWith("\n")) { // Simple trigger for wedge enter key + viewModel.processHardwareScan(it.trim()) + manualInput = "" + } + }, + label = { Text("Scan or Enter Barcode") }, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + singleLine = true, + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = Color.Black, + unfocusedTextColor = Color.Black, + focusedBorderColor = Color(0xFF006D39) + ) + ) + + Button( + onClick = { + viewModel.processHardwareScan(manualInput.trim()) + manualInput = "" + }, + modifier = Modifier.padding(top = 8.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2BAB2B)) + ) { + Text("Enter") + } + + Spacer(modifier = Modifier.height(32.dp)) + + // Feedback Display + val feedback = uiState.pickingFeedback + if (feedback != null) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = if (feedback.contains("Incorrect")) Color(0xFFFFEBEE) else Color(0xFFE8F5E9) + ), + shape = RoundedCornerShape(12.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = feedback, + style = TextStyle( + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = if (feedback.contains("Incorrect")) Color.Red else Color(0xFF2E7D32) + ) + ) + + val product = uiState.lastScannedProduct + if (product != null && !feedback.contains("Incorrect")) { + Spacer(modifier = Modifier.height(16.dp)) + Text(text = "Item: ${product.name}", color = Color.Black, fontWeight = FontWeight.Bold) + + uiState.targetTotes.forEach { pair -> + val toteId = pair.first + val qty = pair.second + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = "Tote $toteId", color = Color.Black, fontSize = 18.sp) + Text(text = "Qty: $qty", color = Color.Black, fontWeight = FontWeight.Bold, fontSize = 18.sp) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + Button( + onClick = { + // Set the target tote for the map highlight + viewModel.updateSelectedToteId(uiState.targetTotes.firstOrNull()?.first) + navController.navigate(Screen.BarcodeMapPicking.route) + }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF006D39)) + ) { + Text("Show on Map") + } + } + } + } + } + } +} diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt index 59d0138..191273b 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt @@ -144,6 +144,13 @@ fun CameraPreviewScreen( val previewView = remember { PreviewView(context) } + // Navigation for Picking Flow + LaunchedEffect(uiState.pickingFeedback) { + if (uiState.pickingFeedback?.startsWith("item identified") == true) { + navController.navigate(Screen.BarcodeMapPicking.route) + } + } + LaunchedEffect(key1 = "clear all the previous results") { // clear all the previous results during Fresh Launch when (uiState.usecaseSelected) { @@ -387,6 +394,31 @@ fun CameraPreviewScreen( TODO("Unhandled usecaseState received = $selectedDemo") } } + + // Show Picking Feedback overlay + if (uiState.selectedCustomer != null && uiState.pickingFeedback != null) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 100.dp), + contentAlignment = Alignment.TopCenter + ) { + Text( + text = uiState.pickingFeedback ?: "", + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ), + modifier = Modifier + .background( + if (uiState.pickingFeedback?.contains("incorrect") == true) Color.Red else Color(0xFF006D39), + RoundedCornerShape(8.dp) + ) + .padding(16.dp) + ) + } + } } uiState.cameraError?.let { diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt new file mode 100644 index 0000000..f9d8b8b --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt @@ -0,0 +1,152 @@ +package com.zebra.aidatacapturedemo.ui.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.zebra.aidatacapturedemo.data.CustomerDataGenerator +import com.zebra.aidatacapturedemo.data.ProductInfo +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import java.util.Locale + +@Composable +fun CustomerInformationScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + @Suppress("UNUSED_PARAMETER") innerPadding: PaddingValues +) { + // Generate data once + val customers = remember { CustomerDataGenerator.generateCustomers() } + + // Store in ViewModel so we can access it during scanning + LaunchedEffect(customers) { + viewModel.setAllCustomers(customers) + } + + // Process data to group by product + val productGroups = remember(customers) { + val groups = mutableMapOf>>() // Barcode to List of (ToteId, Quantity) + val productInfoMap = mutableMapOf() + + customers.forEach { customer -> + customer.products.forEach { product -> + groups.getOrPut(product.barcode) { mutableListOf() }.add(customer.id to product.quantity) + productInfoMap[product.barcode] = product + } + } + + productInfoMap.values.sortedBy { it.name }.map { it to groups[it.barcode]!! } + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFFF8F9FA)) + .padding(top = 40.dp) // Moved down to avoid being blocked + ) { + // Title with bottom border + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .drawBehind { + val borderSize = 1.dp.toPx() + val y = size.height - borderSize / 2 + drawLine( + color = Color.Black, + start = Offset(0f, y), + end = Offset(size.width, y), + strokeWidth = borderSize + ) + } + .padding(bottom = 8.dp) + ) { + Text( + text = "Product Picking List", + style = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + ) + } + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + items(productGroups) { (product, totes) -> + ProductPickingItem(product, totes) + } + + item { + Button( + onClick = { + viewModel.updatePickingFeedback(null) + navController.navigate(Screen.BarcodeScanPicking.route) + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF006D39)) + ) { + Text("Proceed to Scanning", color = Color.White) + } + } + } + } +} + +@Composable +fun ProductPickingItem(product: ProductInfo, totes: List>) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = product.name, + style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold, color = Color.Black) + ) + Text( + text = "Barcode: ${product.barcode} | Price: $${String.format(Locale.US, "%.2f", product.price)}", + style = TextStyle(fontSize = 14.sp, color = Color.Black) + ) + + Spacer(modifier = Modifier.height(8.dp)) + HorizontalDivider(color = Color.LightGray, thickness = 0.5.dp) + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Tote Distribution:", + style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.Black) + ) + + totes.forEach { (toteId, qty) -> + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = "Tote $toteId", style = TextStyle(fontSize = 14.sp, color = Color.Black)) + Text(text = "Qty: $qty", style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Color.Black)) + } + } + } + } +} diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt index fbf46f6..04ad962 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt @@ -33,6 +33,8 @@ sealed class Screen(val route: String) { object OCRBarcodeCapture : Screen("ocrbarcode_capture_screen") object OCRBarcodeResults : Screen("ocrbarcode_results_screen") object BarcodeMapResults : Screen("barcode_map_results_screen") + object CustomerInformation : Screen("customer_information_screen") + object BarcodeScanPicking : Screen("barcode_scan_picking_screen") object BarcodeMapPicking : Screen("barcode_map_picking_screen") object SingleResult : Screen("single_result_screen") @@ -125,6 +127,22 @@ fun NavigationStack( context = context ) } + composable(route = Screen.CustomerInformation.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.CustomerInformation) + CustomerInformationScreen( + viewModel = viewModel, + navController = navController, + innerPadding = innerPadding + ) + } + composable(route = Screen.BarcodeScanPicking.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.BarcodeScanPicking) + BarcodeScanPickingScreen( + viewModel = viewModel, + navController = navController, + innerPadding = innerPadding + ) + } composable(route = Screen.BarcodeMapPicking.route) { viewModel.updateActiveScreenData(activeScreen = Screen.BarcodeMapPicking) BarcodeMapPickingScreen( diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt index 20dcf21..4598e15 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt @@ -50,11 +50,13 @@ import com.zebra.aidatacapturedemo.data.BarcodeFilterData import com.zebra.aidatacapturedemo.data.BarcodeSettings import com.zebra.aidatacapturedemo.data.BarcodeSymbology import com.zebra.aidatacapturedemo.data.CommonSettings +import com.zebra.aidatacapturedemo.data.CustomerInfo import com.zebra.aidatacapturedemo.data.FilterType import com.zebra.aidatacapturedemo.data.ModuleData import com.zebra.aidatacapturedemo.data.OcrBarcodeFindSettings import com.zebra.aidatacapturedemo.data.OcrFilterData import com.zebra.aidatacapturedemo.data.ProductData +import com.zebra.aidatacapturedemo.data.ProductInfo import com.zebra.aidatacapturedemo.data.ProductRecognitionSettings import com.zebra.aidatacapturedemo.data.ResultData import com.zebra.aidatacapturedemo.data.RetailShelfSettings @@ -1787,6 +1789,70 @@ class AIDataCaptureDemoViewModel( barcodeResults = results ) } + + // Handle Picking Logic if we are in picking flow + if (uiState.value.selectedCustomer != null && results.isNotEmpty()) { + handlePickingScan(results) + } + } + + private fun handlePickingScan(results: List) { + if (results.isEmpty()) return + + val scannedBarcode = results.first().text + val customer = uiState.value.selectedCustomer ?: return + + val productMatch = customer.products.find { it.barcode == scannedBarcode } + + if (productMatch != null) { + _uiState.update { it.copy( + pickingFeedback = "Item Identified Barcode: $scannedBarcode", + selectedToteId = scannedBarcode // Highlight it on the map + ) } + } else { + _uiState.update { it.copy( + pickingFeedback = "Incorrect Item" + ) } + } + } + + fun updateSelectedCustomer(customer: com.zebra.aidatacapturedemo.data.CustomerInfo?) { + _uiState.update { it.copy(selectedCustomer = customer) } + } + + fun updatePickingFeedback(feedback: String?) { + _uiState.update { it.copy(pickingFeedback = feedback) } + } + + fun setAllCustomers(customers: List) { + _uiState.update { it.copy(allCustomers = customers) } + } + + fun processHardwareScan(barcode: String) { + val customers = uiState.value.allCustomers + val matches = mutableListOf>() + var productInfo: ProductInfo? = null + + customers.forEach { customer -> + customer.products.find { it.barcode == barcode }?.let { product -> + matches.add(customer.id to product.quantity) + productInfo = product + } + } + + if (matches.isNotEmpty()) { + _uiState.update { it.copy( + lastScannedProduct = productInfo, + targetTotes = matches, + pickingFeedback = "Item Identified Barcode: $barcode" + ) } + } else { + _uiState.update { it.copy( + lastScannedProduct = null, + targetTotes = listOf(), + pickingFeedback = "Incorrect Item" + ) } + } } fun updateRetailShelfDetectionResult(results: Array?) { diff --git a/AISuite_QuickStart/.DS_Store b/AISuite_QuickStart/.DS_Store index 2993e12fd8a95991bea9c0887fcfc6fcbc8d43bf..7680c78d1cbdad78e2e033335eaa0901e54e6d9f 100644 GIT binary patch delta 83 zcmZoMXfc=|#>B`mu~2NHo}wrR0|Nsi1A_pAVQ_MOZUKB)qu~2NHo}wrZ0|Nsi1A_nqLkdF2~lb-~X;lQn`AOmDcZUImg p&~gKY%{+|Rtee?6_&I>?0^0qZc{0C Date: Thu, 4 Jun 2026 10:54:16 -0400 Subject: [PATCH 06/47] Added customer information page and product page with product name, price, and barcode number (cherry picked from commit a6eda734be19080554e66905db8d2062d826fb44) --- .../data/AIDataCaptureDemoUiState.kt | 11 +- .../aidatacapturedemo/data/CustomerInfo.kt | 36 +++ .../ui/view/BarcodeMapPickingScreen.kt | 263 ++++++++++++++++++ .../ui/view/BarcodeMapResultScreen.kt | 247 +++++++++------- .../ui/view/BarcodeScanPickingScreen.kt | 160 +++++++++++ .../ui/view/CameraPreviewScreen.kt | 32 +++ .../ui/view/CustomerInformationScreen.kt | 152 ++++++++++ .../ui/view/NavigationStack.kt | 29 ++ .../viewmodel/AIDataCaptureDemoViewModel.kt | 66 +++++ AISuite_QuickStart/.DS_Store | Bin 0 -> 6148 bytes 10 files changed, 886 insertions(+), 110 deletions(-) create mode 100644 AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/CustomerInfo.kt create mode 100644 AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt create mode 100644 AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeScanPickingScreen.kt create mode 100644 AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt create mode 100644 AISuite_QuickStart/.DS_Store diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt index e7378e5..b3fb4a9 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt @@ -9,11 +9,6 @@ import com.zebra.aidatacapturedemo.ui.view.Screen /** * AIDataCaptureDemoUiState.kt is a data class that holds the UI state for the AI Data Capture Demo - * application. It includes various settings and results related to barcode scanning, OCR, - * Retail shelf recognition, and Product recognition. The state is updated based on user - * interactions and model outputs, allowing the UI to reactively display the current status of the - * application. This class is used to manage the state of the application and facilitate - * communication between the UI and the underlying models. */ val PROFILING = "Profiling" @@ -192,6 +187,12 @@ data class AIDataCaptureDemoUiState( var productResults: MutableList = mutableListOf(), val ocrResults: List = listOf(), var barcodeResults: List = listOf(), + var selectedToteId: String? = null, + var allCustomers: List = listOf(), + var selectedCustomer: CustomerInfo? = null, + var pickingFeedback: String? = null, + var lastScannedProduct: ProductInfo? = null, + var targetTotes: List> = listOf(), // Tote ID to Quantity // Choices var isBarcodeModelEnabled: Boolean = true, diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/CustomerInfo.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/CustomerInfo.kt new file mode 100644 index 0000000..e392831 --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/CustomerInfo.kt @@ -0,0 +1,36 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.data + +import kotlin.random.Random + +data class ProductInfo( + val name: String, + val price: Double, + val barcode: String, + val quantity: Int = Random.nextInt(1, 10) +) + +data class CustomerInfo( + val id: String, + val products: List +) + +object CustomerDataGenerator { + private val availableProducts = listOf( + ProductInfo("Heidrun opbergbox met klapedeksel", 2.99, "2540068"), + ProductInfo("Opbergbox+klemdeksel A4", 2.49, "2543429"), + ProductInfo("Onderbedboxklemdeksel", 5.95, "2568528"), + ProductInfo("Opbergbox met klemdeksel", 7.49, "3205800") + ) + + fun generateCustomers(): List { + return listOf("A", "B", "C", "D", "E", "F").map { id -> + val numProducts = Random.nextInt(1, 4) + val selectedProducts = availableProducts.shuffled().take(numProducts).map { + it.copy(quantity = Random.nextInt(1, 6)) + } + CustomerInfo(id, selectedProducts) + } + } +} diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt new file mode 100644 index 0000000..820e903 --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt @@ -0,0 +1,263 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.ui.view + +import android.content.Context +import android.view.WindowManager +import android.view.WindowMetrics +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat.getSystemService +import androidx.lifecycle.Lifecycle +import androidx.navigation.NavController +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import kotlin.math.min +import androidx.compose.ui.graphics.drawscope.DrawScope +import com.zebra.aidatacapturedemo.data.ResultData +import kotlin.math.abs + +@Composable +fun BarcodeMapPickingScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + @Suppress("UNUSED_PARAMETER") context: Context, + @Suppress("UNUSED_PARAMETER") activityInnerPadding: PaddingValues, + @Suppress("UNUSED_PARAMETER") activityLifecycle: Lifecycle +) { + val uiState by viewModel.uiState.collectAsState() + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + viewModel.updateAppBarTitle("Item Picking") + + // Logic to "decide which tote": + // In this demo, if a barcode is detected, we match it to a tote. + LaunchedEffect(uiState.barcodeResults) { + if (uiState.barcodeResults.isNotEmpty()) { + val detectedText = uiState.barcodeResults.first().text + // In a real app, we'd lookup which tote 'detectedText' belongs to. + // For this demo, let's just use the first detected one as the target + viewModel.updateSelectedToteId(detectedText) + } + } + + Box(modifier = Modifier.fillMaxSize()) { + // 1. Full screen Abstract Map (The "Digital Twin") + AbstractMapLayer(uiState) + + // 2. Guidance Overlay + val feedback = uiState.pickingFeedback + if (feedback != null) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 100.dp), + contentAlignment = Alignment.TopCenter + ) { + Text( + text = feedback, + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ), + modifier = Modifier + .background( + if (feedback.contains("incorrect")) Color.Red else Color(0xFF006D39), + RoundedCornerShape(8.dp) + ) + .padding(16.dp) + ) + } + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 100.dp), + contentAlignment = Alignment.TopCenter + ) { + Text( + text = "Scan Item Barcode", + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ), + modifier = Modifier + .background(Color.White.copy(alpha = 0.8f), RoundedCornerShape(8.dp)) + .padding(16.dp) + ) + } + } + } +} + +@Composable +private fun AbstractMapLayer(uiState: AIDataCaptureDemoUiState) { + val capturedBitmap = uiState.captureBitmap ?: return + + val displayMetrics = LocalContext.current.resources.displayMetrics + val displayMetricsDensity = displayMetrics.density + val windowManager = getSystemService(LocalContext.current, WindowManager::class.java) + val windowMetrics: WindowMetrics = windowManager!!.currentWindowMetrics + val displayTotalWidthInPx = windowMetrics.bounds.width() + val displayTotalHeightInPx = windowMetrics.bounds.height() + + // Simplified scaling logic for the abstract map + val scaler = min( + displayTotalWidthInPx.toFloat() / capturedBitmap.width.toFloat(), + displayTotalHeightInPx.toFloat() / capturedBitmap.height.toFloat() + ) + val gapX = (displayTotalWidthInPx - (scaler * capturedBitmap.width.toFloat())) / 2f + val gapY = (displayTotalHeightInPx - (scaler * capturedBitmap.height.toFloat())) / 2f + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFFF0F2F5)) + ) { + DrawAbstractBarcodeMapLayer( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + } +} + +@Composable +private fun DrawAbstractBarcodeMapLayer( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float +) { + val barcodeResults = uiState.barcodeResults + if (barcodeResults.isEmpty()) return + + // Grouping logic for rows (Reusing logic from Result screen) + val sortedByY = barcodeResults.sortedBy { it.boundingBox.centerY() } + val rows = mutableListOf>() + + if (sortedByY.isNotEmpty()) { + var currentRow = mutableListOf() + currentRow.add(sortedByY[0]) + rows.add(currentRow) + + for (i in 1 until sortedByY.size) { + val prev = sortedByY[i - 1] + val curr = sortedByY[i] + if (abs(curr.boundingBox.centerY() - prev.boundingBox.centerY()) < (prev.boundingBox.height() * 0.6)) { + currentRow.add(curr) + } else { + currentRow = mutableListOf() + currentRow.add(curr) + rows.add(currentRow) + } + } + } + + Canvas(modifier = Modifier.fillMaxSize()) { + rows.forEach { row -> + val sortedRow = row.sortedBy { it.boundingBox.left } + val avgHeight = sortedRow.map { it.boundingBox.height() }.average().toFloat() + val avgCenterY = sortedRow.map { it.boundingBox.centerY() }.average().toFloat() + + var currentLeftX = -1f + + sortedRow.forEach { barcode -> + val bBoxWidth = barcode.boundingBox.width().toFloat() + var left = barcode.boundingBox.left.toFloat() + + if (currentLeftX != -1f) { + if (abs(left - currentLeftX) < bBoxWidth * 0.4) { + left = currentLeftX + } + } + + val scaledLeft = (scaler * left) + gapX + val scaledTop = (scaler * (avgCenterY - avgHeight/2)) + gapY + val scaledWidth = (scaler * bBoxWidth) + val scaledHeight = (scaler * avgHeight) + + // Highlight if it's the selected tote + val isTarget = uiState.selectedToteId == barcode.text + + drawAbstractPickingUnit( + id = barcode.text, + left = scaledLeft, + top = scaledTop, + width = scaledWidth, + height = scaledHeight, + density = displayMetricsDensity, + isTarget = isTarget + ) + + currentLeftX = left + bBoxWidth + } + } + } +} + +private fun DrawScope.drawAbstractPickingUnit( + id: String, + left: Float, + top: Float, + width: Float, + height: Float, + density: Float, + isTarget: Boolean +) { + val themeColor = if (isTarget) Color(0xFFFFCC00) else Color(0xFF00FF00) // Gold for target, Green for others + val rectSize = androidx.compose.ui.geometry.Size(width, height) + val topLeft = Offset(left, top) + + drawRect( + color = themeColor.copy(alpha = if (isTarget) 0.8f else 0.2f), + topLeft = topLeft, + size = rectSize + ) + + drawRect( + color = if (isTarget) Color.Red else themeColor, + topLeft = topLeft, + size = rectSize, + style = Stroke(width = (if (isTarget) 4f else 2f) * density) + ) + + val paint = android.graphics.Paint().apply { + this.color = if (isTarget) android.graphics.Color.WHITE else android.graphics.Color.BLACK + this.textSize = (if (isTarget) 12f else 9f) * density + this.textAlign = android.graphics.Paint.Align.CENTER + this.isAntiAlias = true + this.isFakeBoldText = true + } + + val textX = left + width / 2 + val textY = top + height / 2 - (paint.fontMetrics.ascent + paint.fontMetrics.descent) / 2 + + if (width > 25 * density) { + val displayId = if (id.length > 7) id.take(5) + ".." else id + drawContext.canvas.nativeCanvas.drawText(displayId, textX, textY, paint) + } +} diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt index 6829764..579e15e 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -34,7 +33,6 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.nativeCanvas -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -45,10 +43,13 @@ import androidx.navigation.NavController import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel import kotlin.math.min +import androidx.compose.ui.graphics.drawscope.DrawScope +import com.zebra.aidatacapturedemo.data.ResultData +import kotlin.math.abs /** - * BarcodeMapResultScreen is a Composable function that displays the relative positions - * of the detected barcodes and their IDs on a clean background. + * BarcodeMapResultScreen is a Composable function that displays an abstract + * geometrical layout of detected barcodes on a clean background. */ @Composable fun BarcodeMapResultScreen( @@ -62,7 +63,7 @@ fun BarcodeMapResultScreen( BackHandler(enabled = true) { viewModel.handleBackButton(navController) } - viewModel.updateAppBarTitle("Barcode Map") + viewModel.updateAppBarTitle("Barcode Layout Map") val capturedBitmap = uiState.captureBitmap if (capturedBitmap == null) { @@ -99,35 +100,29 @@ fun BarcodeMapResultScreen( val availableHeightInPx = displayTotalHeightInPx.toFloat() - displayStatusBarHeightInPx - displayNavigationBarHeightInPx - // The following computed values are used for drawing Bbox overlay + // The following computed values are used for drawing val scaler = min( displayTotalWidthInPx.toFloat() / capturedBitmap.width.toFloat(), availableHeightInPx / capturedBitmap.height.toFloat() ) - val scaledWidth = scaler * capturedBitmap.width.toFloat() - val scaledHeight = scaler * capturedBitmap.height.toFloat() - val gapX = (displayTotalWidthInPx - scaledWidth) / 2f - val gapY = (availableHeightInPx - scaledHeight) / 2f + val gapX = (displayTotalWidthInPx - (scaler * capturedBitmap.width.toFloat())) / 2f + val gapY = (availableHeightInPx - (scaler * capturedBitmap.height.toFloat())) / 2f - - Box( // Bottom layer + Box( modifier = Modifier .fillMaxSize() .padding( top = displayStatusBarHeightInDp, bottom = displayNavigationBarHeightInDp ) - .background(color = Color(0xFFF0F0F0)) // Clean light gray background + .background(color = Color(0xFFF0F2F5)) // Clean modern background ) { - - // MAP CANVAS + // ABSTRACT MAP CANVAS Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - // We don't draw the captured image anymore as per user request - - DrawBarcodeMapOnCanvas( + DrawAbstractBarcodeMap( uiState = uiState, scaler = scaler, gapX = gapX, @@ -140,44 +135,52 @@ fun BarcodeMapResultScreen( Box( modifier = Modifier .fillMaxWidth() - .padding(top = 16.dp), + .padding(top = 24.dp), contentAlignment = Alignment.TopCenter ) { - Text( - text = "Detected ${uiState.barcodeResults.size} Barcodes", - style = TextStyle( - fontSize = 18.sp, - fontWeight = FontWeight.Bold, - color = Color.Black - ), - modifier = Modifier - .background(Color.White.copy(alpha = 0.8f), RoundedCornerShape(8.dp)) - .padding(horizontal = 16.dp, vertical = 8.dp) - ) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "Barcode Layout Map", + style = TextStyle( + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1A1C1E) + ) + ) + Text( + text = "${uiState.barcodeResults.size} barcodes mapped", + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = Color(0xFF44474E) + ) + ) + } } // SAVE BUTTON Box( modifier = Modifier .fillMaxSize() - .padding(bottom = 24.dp), + .padding(bottom = 32.dp), contentAlignment = Alignment.BottomCenter ) { Button( onClick = { viewModel.saveBarcodeLayout() + navController.navigate(Screen.CustomerInformation.route) }, modifier = Modifier - .fillMaxWidth(0.7f) - .height(56.dp), - shape = RoundedCornerShape(28.dp), - colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF006400)) + .fillMaxWidth(0.6f) + .height(54.dp), + shape = RoundedCornerShape(27.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF006D39)) // Dark green ) { Text( - text = "Save Layout", + text = "Save Barcode Map", style = TextStyle( - fontSize = 18.sp, - fontWeight = FontWeight.Bold, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, color = Color.White ) ) @@ -188,90 +191,124 @@ fun BarcodeMapResultScreen( } @Composable -private fun DrawBarcodeMapOnCanvas( +private fun DrawAbstractBarcodeMap( uiState: AIDataCaptureDemoUiState, scaler: Float, gapX: Float, gapY: Float, displayMetricsDensity: Float ) { - Canvas( - modifier = Modifier - .fillMaxSize() - ) { - uiState.barcodeResults.forEachIndexed { index, barcodeData -> - barcodeData?.let { + val barcodeResults = uiState.barcodeResults + if (barcodeResults.isEmpty()) return + + // Grouping logic for rows + val sortedByY = barcodeResults.sortedBy { it.boundingBox.centerY() } + val rows = mutableListOf>() + + if (sortedByY.isNotEmpty()) { + var currentRow = mutableListOf() + currentRow.add(sortedByY[0]) + rows.add(currentRow) + + for (i in 1 until sortedByY.size) { + val prev = sortedByY[i - 1] + val curr = sortedByY[i] + + // Overlap threshold for same row + if (abs(curr.boundingBox.centerY() - prev.boundingBox.centerY()) < (prev.boundingBox.height() * 0.6)) { + currentRow.add(curr) + } else { + currentRow = mutableListOf() + currentRow.add(curr) + rows.add(currentRow) + } + } + } - val bBoxTop = barcodeData.boundingBox.top.toFloat() - val bBoxLeft = barcodeData.boundingBox.left.toFloat() - val bBoxBottom = barcodeData.boundingBox.bottom.toFloat() - val bBoxRight = barcodeData.boundingBox.right.toFloat() + Canvas(modifier = Modifier.fillMaxSize()) { + rows.forEach { row -> + val sortedRow = row.sortedBy { it.boundingBox.left } + + // Normalize row metrics to average + val avgHeight = sortedRow.map { it.boundingBox.height() }.average().toFloat() + val avgCenterY = sortedRow.map { it.boundingBox.centerY() }.average().toFloat() - val scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX - val scaledBBoxTopInPx = (scaler * bBoxTop) + gapY - val scaledBBoxRightInPx = (scaler * bBoxRight) + gapX - val scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + var currentLeftX = -1f - // Define the size and position of the rectangle - val rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx - val rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx - val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + sortedRow.forEachIndexed { index, barcode -> + val bBoxWidth = barcode.boundingBox.width().toFloat() + var left = barcode.boundingBox.left.toFloat() + + // Snapping logic: if close to previous, snap to it + if (currentLeftX != -1f) { + if (abs(left - currentLeftX) < bBoxWidth * 0.4) { + left = currentLeftX + } + } - // 1. Draw Bounding Box - drawRect( - color = Color(0xFF00FF00), // zebra green - topLeft = topLeftOffset, - size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), - style = Stroke(width = (2f * displayMetricsDensity)) - ) + val scaledLeft = (scaler * left) + gapX + val scaledTop = (scaler * (avgCenterY - avgHeight/2)) + gapY + val scaledWidth = (scaler * bBoxWidth) + val scaledHeight = (scaler * avgHeight) - // 2. Draw semi-transparent overlay inside the box - drawRect( - color = Color(0x3300FF00), - topLeft = topLeftOffset, - size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight) + drawAbstractUnit( + id = barcode.text, + left = scaledLeft, + top = scaledTop, + width = scaledWidth, + height = scaledHeight, + density = displayMetricsDensity ) + + // Track where the next one should start if it snaps + currentLeftX = left + bBoxWidth + } + } + } +} - // 3. Prepare label text - val idText = if (barcodeData.text.isNotEmpty()) barcodeData.text else "N/A" - val labelText = "#${index + 1}: $idText" - - val paint = android.graphics.Paint().apply { - color = android.graphics.Color.WHITE - textSize = 14f * displayMetricsDensity - typeface = android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD) - isAntiAlias = true - } +private fun DrawScope.drawAbstractUnit( + id: String, + left: Float, + top: Float, + width: Float, + height: Float, + density: Float +) { + val themeColor = Color(0xFF00FF00) // Vibrant Green + val rectSize = androidx.compose.ui.geometry.Size(width, height) + val topLeft = Offset(left, top) - val textWidth = paint.measureText(labelText) - val fontMetrics = paint.fontMetrics - val textHeight = fontMetrics.descent - fontMetrics.ascent + // 1. Draw simple geometrical shape (Rectangle) + drawRect( + color = themeColor.copy(alpha = 0.2f), + topLeft = topLeft, + size = rectSize + ) - // Position label above the barcode - val padding = 4f * displayMetricsDensity - val labelX = scaledBBoxLeftInPx - var labelY = scaledBBoxTopInPx - textHeight - (2 * padding) + // 2. Draw sharp outline + drawRect( + color = themeColor, + topLeft = topLeft, + size = rectSize, + style = Stroke(width = 2f * density) + ) - // Ensure label is within screen bounds - if (labelY < 0) { - labelY = scaledBBoxBottomInPx + padding - } + // 3. Center-aligned ID text + val paint = android.graphics.Paint().apply { + this.color = android.graphics.Color.BLACK + this.textSize = 9f * density + this.textAlign = android.graphics.Paint.Align.CENTER + this.isAntiAlias = true + this.isFakeBoldText = true + } - // Draw label background - drawRect( - color = Color(0xFF006400), // Darker green - topLeft = Offset(labelX, labelY), - size = androidx.compose.ui.geometry.Size(textWidth + (2 * padding), textHeight + padding) - ) + val textX = left + width / 2 + val textY = top + height / 2 - (paint.fontMetrics.ascent + paint.fontMetrics.descent) / 2 - // Draw text - drawContext.canvas.nativeCanvas.drawText( - labelText, - labelX + padding, - labelY + textHeight - fontMetrics.descent, - paint - ) - } - } + // Only draw ID if it fits within the simplified shape + if (width > 25 * density) { + val displayId = if (id.length > 7) id.take(5) + ".." else id + drawContext.canvas.nativeCanvas.drawText(displayId, textX, textY, paint) } } diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeScanPickingScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeScanPickingScreen.kt new file mode 100644 index 0000000..9478971 --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeScanPickingScreen.kt @@ -0,0 +1,160 @@ +package com.zebra.aidatacapturedemo.ui.view + +import android.content.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.ProductInfo +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +@Composable +fun BarcodeScanPickingScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + @Suppress("UNUSED_PARAMETER") innerPadding: PaddingValues +) { + val uiState by viewModel.uiState.collectAsState() + val context = LocalContext.current + val focusRequester = remember { FocusRequester() } + var manualInput by remember { mutableStateOf("") } + + // Register BroadcastReceiver for DataWedge + DisposableEffect(Unit) { + val filter = IntentFilter("com.zebra.aidatacapturedemo.SCAN") + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val scanData = intent?.getStringExtra("com.symbol.datawedge.data_string") + if (scanData != null) { + viewModel.processHardwareScan(scanData) + } + } + } + context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED) + onDispose { + context.unregisterReceiver(receiver) + } + } + + // Auto-focus the manual input field to capture keyboard wedge scans + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFFF0F2F5)) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Ready to Scan", + style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color.Black), + modifier = Modifier.padding(top = 32.dp, bottom = 16.dp) + ) + + // Invisible or small TextField to capture keyboard wedge input + OutlinedTextField( + value = manualInput, + onValueChange = { + manualInput = it + if (it.endsWith("\n")) { // Simple trigger for wedge enter key + viewModel.processHardwareScan(it.trim()) + manualInput = "" + } + }, + label = { Text("Scan or Enter Barcode") }, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + singleLine = true, + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = Color.Black, + unfocusedTextColor = Color.Black, + focusedBorderColor = Color(0xFF006D39) + ) + ) + + Button( + onClick = { + viewModel.processHardwareScan(manualInput.trim()) + manualInput = "" + }, + modifier = Modifier.padding(top = 8.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2BAB2B)) + ) { + Text("Enter") + } + + Spacer(modifier = Modifier.height(32.dp)) + + // Feedback Display + val feedback = uiState.pickingFeedback + if (feedback != null) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = if (feedback.contains("Incorrect")) Color(0xFFFFEBEE) else Color(0xFFE8F5E9) + ), + shape = RoundedCornerShape(12.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = feedback, + style = TextStyle( + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = if (feedback.contains("Incorrect")) Color.Red else Color(0xFF2E7D32) + ) + ) + + val product = uiState.lastScannedProduct + if (product != null && !feedback.contains("Incorrect")) { + Spacer(modifier = Modifier.height(16.dp)) + Text(text = "Item: ${product.name}", color = Color.Black, fontWeight = FontWeight.Bold) + + uiState.targetTotes.forEach { pair -> + val toteId = pair.first + val qty = pair.second + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = "Tote $toteId", color = Color.Black, fontSize = 18.sp) + Text(text = "Qty: $qty", color = Color.Black, fontWeight = FontWeight.Bold, fontSize = 18.sp) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + Button( + onClick = { + // Set the target tote for the map highlight + viewModel.updateSelectedToteId(uiState.targetTotes.firstOrNull()?.first) + navController.navigate(Screen.BarcodeMapPicking.route) + }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF006D39)) + ) { + Text("Show on Map") + } + } + } + } + } + } +} diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt index 59d0138..191273b 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt @@ -144,6 +144,13 @@ fun CameraPreviewScreen( val previewView = remember { PreviewView(context) } + // Navigation for Picking Flow + LaunchedEffect(uiState.pickingFeedback) { + if (uiState.pickingFeedback?.startsWith("item identified") == true) { + navController.navigate(Screen.BarcodeMapPicking.route) + } + } + LaunchedEffect(key1 = "clear all the previous results") { // clear all the previous results during Fresh Launch when (uiState.usecaseSelected) { @@ -387,6 +394,31 @@ fun CameraPreviewScreen( TODO("Unhandled usecaseState received = $selectedDemo") } } + + // Show Picking Feedback overlay + if (uiState.selectedCustomer != null && uiState.pickingFeedback != null) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 100.dp), + contentAlignment = Alignment.TopCenter + ) { + Text( + text = uiState.pickingFeedback ?: "", + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ), + modifier = Modifier + .background( + if (uiState.pickingFeedback?.contains("incorrect") == true) Color.Red else Color(0xFF006D39), + RoundedCornerShape(8.dp) + ) + .padding(16.dp) + ) + } + } } uiState.cameraError?.let { diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt new file mode 100644 index 0000000..f9d8b8b --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt @@ -0,0 +1,152 @@ +package com.zebra.aidatacapturedemo.ui.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.zebra.aidatacapturedemo.data.CustomerDataGenerator +import com.zebra.aidatacapturedemo.data.ProductInfo +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import java.util.Locale + +@Composable +fun CustomerInformationScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + @Suppress("UNUSED_PARAMETER") innerPadding: PaddingValues +) { + // Generate data once + val customers = remember { CustomerDataGenerator.generateCustomers() } + + // Store in ViewModel so we can access it during scanning + LaunchedEffect(customers) { + viewModel.setAllCustomers(customers) + } + + // Process data to group by product + val productGroups = remember(customers) { + val groups = mutableMapOf>>() // Barcode to List of (ToteId, Quantity) + val productInfoMap = mutableMapOf() + + customers.forEach { customer -> + customer.products.forEach { product -> + groups.getOrPut(product.barcode) { mutableListOf() }.add(customer.id to product.quantity) + productInfoMap[product.barcode] = product + } + } + + productInfoMap.values.sortedBy { it.name }.map { it to groups[it.barcode]!! } + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFFF8F9FA)) + .padding(top = 40.dp) // Moved down to avoid being blocked + ) { + // Title with bottom border + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .drawBehind { + val borderSize = 1.dp.toPx() + val y = size.height - borderSize / 2 + drawLine( + color = Color.Black, + start = Offset(0f, y), + end = Offset(size.width, y), + strokeWidth = borderSize + ) + } + .padding(bottom = 8.dp) + ) { + Text( + text = "Product Picking List", + style = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + ) + } + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + items(productGroups) { (product, totes) -> + ProductPickingItem(product, totes) + } + + item { + Button( + onClick = { + viewModel.updatePickingFeedback(null) + navController.navigate(Screen.BarcodeScanPicking.route) + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF006D39)) + ) { + Text("Proceed to Scanning", color = Color.White) + } + } + } + } +} + +@Composable +fun ProductPickingItem(product: ProductInfo, totes: List>) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = product.name, + style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold, color = Color.Black) + ) + Text( + text = "Barcode: ${product.barcode} | Price: $${String.format(Locale.US, "%.2f", product.price)}", + style = TextStyle(fontSize = 14.sp, color = Color.Black) + ) + + Spacer(modifier = Modifier.height(8.dp)) + HorizontalDivider(color = Color.LightGray, thickness = 0.5.dp) + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Tote Distribution:", + style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.Black) + ) + + totes.forEach { (toteId, qty) -> + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = "Tote $toteId", style = TextStyle(fontSize = 14.sp, color = Color.Black)) + Text(text = "Qty: $qty", style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Color.Black)) + } + } + } + } +} diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt index da15fc9..04ad962 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt @@ -33,6 +33,9 @@ sealed class Screen(val route: String) { object OCRBarcodeCapture : Screen("ocrbarcode_capture_screen") object OCRBarcodeResults : Screen("ocrbarcode_results_screen") object BarcodeMapResults : Screen("barcode_map_results_screen") + object CustomerInformation : Screen("customer_information_screen") + object BarcodeScanPicking : Screen("barcode_scan_picking_screen") + object BarcodeMapPicking : Screen("barcode_map_picking_screen") object SingleResult : Screen("single_result_screen") /** @@ -124,6 +127,32 @@ fun NavigationStack( context = context ) } + composable(route = Screen.CustomerInformation.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.CustomerInformation) + CustomerInformationScreen( + viewModel = viewModel, + navController = navController, + innerPadding = innerPadding + ) + } + composable(route = Screen.BarcodeScanPicking.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.BarcodeScanPicking) + BarcodeScanPickingScreen( + viewModel = viewModel, + navController = navController, + innerPadding = innerPadding + ) + } + composable(route = Screen.BarcodeMapPicking.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.BarcodeMapPicking) + BarcodeMapPickingScreen( + viewModel = viewModel, + navController = navController, + context = context, + activityInnerPadding = activityInnerPadding, + activityLifecycle = activityLifecycle + ) + } composable(route = Screen.SingleResult.route + "?text={text}&bbox={bbox}&isBarcode={isBarcode}") { backStackEntry -> viewModel.updateActiveScreenData(activeScreen = Screen.SingleResult) val text = backStackEntry.arguments?.getString("text") ?: "" diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt index 6eeafe7..fca9a99 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt @@ -50,11 +50,13 @@ import com.zebra.aidatacapturedemo.data.BarcodeFilterData import com.zebra.aidatacapturedemo.data.BarcodeSettings import com.zebra.aidatacapturedemo.data.BarcodeSymbology import com.zebra.aidatacapturedemo.data.CommonSettings +import com.zebra.aidatacapturedemo.data.CustomerInfo import com.zebra.aidatacapturedemo.data.FilterType import com.zebra.aidatacapturedemo.data.ModuleData import com.zebra.aidatacapturedemo.data.OcrBarcodeFindSettings import com.zebra.aidatacapturedemo.data.OcrFilterData import com.zebra.aidatacapturedemo.data.ProductData +import com.zebra.aidatacapturedemo.data.ProductInfo import com.zebra.aidatacapturedemo.data.ProductRecognitionSettings import com.zebra.aidatacapturedemo.data.ResultData import com.zebra.aidatacapturedemo.data.RetailShelfSettings @@ -1787,6 +1789,70 @@ class AIDataCaptureDemoViewModel( barcodeResults = results ) } + + // Handle Picking Logic if we are in picking flow + if (uiState.value.selectedCustomer != null && results.isNotEmpty()) { + handlePickingScan(results) + } + } + + private fun handlePickingScan(results: List) { + if (results.isEmpty()) return + + val scannedBarcode = results.first().text + val customer = uiState.value.selectedCustomer ?: return + + val productMatch = customer.products.find { it.barcode == scannedBarcode } + + if (productMatch != null) { + _uiState.update { it.copy( + pickingFeedback = "Item Identified Barcode: $scannedBarcode", + selectedToteId = scannedBarcode // Highlight it on the map + ) } + } else { + _uiState.update { it.copy( + pickingFeedback = "Incorrect Item" + ) } + } + } + + fun updateSelectedCustomer(customer: com.zebra.aidatacapturedemo.data.CustomerInfo?) { + _uiState.update { it.copy(selectedCustomer = customer) } + } + + fun updatePickingFeedback(feedback: String?) { + _uiState.update { it.copy(pickingFeedback = feedback) } + } + + fun setAllCustomers(customers: List) { + _uiState.update { it.copy(allCustomers = customers) } + } + + fun processHardwareScan(barcode: String) { + val customers = uiState.value.allCustomers + val matches = mutableListOf>() + var productInfo: ProductInfo? = null + + customers.forEach { customer -> + customer.products.find { it.barcode == barcode }?.let { product -> + matches.add(customer.id to product.quantity) + productInfo = product + } + } + + if (matches.isNotEmpty()) { + _uiState.update { it.copy( + lastScannedProduct = productInfo, + targetTotes = matches, + pickingFeedback = "Item Identified Barcode: $barcode" + ) } + } else { + _uiState.update { it.copy( + lastScannedProduct = null, + targetTotes = listOf(), + pickingFeedback = "Incorrect Item" + ) } + } } fun updateRetailShelfDetectionResult(results: Array?) { diff --git a/AISuite_QuickStart/.DS_Store b/AISuite_QuickStart/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..7680c78d1cbdad78e2e033335eaa0901e54e6d9f GIT binary patch literal 6148 zcmeHKy-ve05Wb@oio{YemCY*~o#_y&@B&m~V1g3V4v7*~V$ahc-h_d-;1zfQzPod1 z0)Y`BxRdP9z8|0SRrT3K#EbiUMl>O!1Wk}-Fd(8HH0?!aF|w>NTi#Y>t=h|I`|Tim zAJGlfR2S>+{+}Orma@vSRZ*>=NsdkrA2u&fXI*RljkVr%R=qkw812)Vw#b=NL3h2Z z^g91&@8&+fn6|llYn0V)2VeR0_QCRdyEp^RfHU9>{8I){vqh2vL+_mdXTTYFXF$$} zfF_tmYz+12K&MXt;23HZjP-5_459$05gS8jAZ(#P3+4F4U<-#nM87m*V`$;Tni=cF z%GA ywE^uKO+@?}89*@BQVi5mjES5hQ#G1B$QbrCVq++)h+e~iei2YYymJPAfPptNu0_BA literal 0 HcmV?d00001 From ccaf67263fb66bfa7257b35340c6bca96a3ba2d0 Mon Sep 17 00:00:00 2001 From: cachen-hi Date: Thu, 4 Jun 2026 11:16:45 -0400 Subject: [PATCH 07/47] fixed the tote and connected them --- .../aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt index fca9a99..5f29243 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt @@ -1824,6 +1824,10 @@ class AIDataCaptureDemoViewModel( _uiState.update { it.copy(pickingFeedback = feedback) } } + fun updateSelectedToteId(toteId: String?) { + _uiState.update { it.copy(selectedToteId = toteId) } + } + fun setAllCustomers(customers: List) { _uiState.update { it.copy(allCustomers = customers) } } From a6dbd5beec152108dd70cbdbbbd8a952a4748ee7 Mon Sep 17 00:00:00 2001 From: cachen-hi Date: Thu, 4 Jun 2026 12:53:42 -0400 Subject: [PATCH 08/47] deleted unused import and changed the name the Scan picking screen --- ...rcodeScanPickingScreen.kt => BarcodeMapScanPickingScreen.kt} | 2 -- 1 file changed, 2 deletions(-) rename AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/{BarcodeScanPickingScreen.kt => BarcodeMapScanPickingScreen.kt} (98%) diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeScanPickingScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapScanPickingScreen.kt similarity index 98% rename from AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeScanPickingScreen.kt rename to AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapScanPickingScreen.kt index 9478971..c3d8a72 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeScanPickingScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapScanPickingScreen.kt @@ -17,8 +17,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState -import com.zebra.aidatacapturedemo.data.ProductInfo import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel @Composable From e3560a7c70513c6186e060af980c45fb2409840f Mon Sep 17 00:00:00 2001 From: cachen-hi Date: Thu, 4 Jun 2026 15:47:18 -0400 Subject: [PATCH 09/47] deleted a space Added a folder for project two that has the same feature with AI data capture --- .../ui/view/BarcodeMapPickingScreen.kt | 14 +- AISuite_Demos/Project 2/Project 2/.gitignore | 15 + AISuite_Demos/Project 2/Project 2/README.md | 128 + .../Project 2/Project 2/app/.gitignore | 1 + .../Project 2/Project 2/app/build.gradle.kts | 133 ++ .../Project 2/app/proguard-rules.pro | 21 + .../app/src/main/AndroidManifest.xml | 60 + .../main/assets/barcode_model_input_size.html | 70 + .../src/main/assets/barcode_resolution.html | 85 + .../src/main/assets/barcode_symbologies.html | 53 + .../src/main/assets/ocr_model_input_size.html | 60 + .../app/src/main/assets/ocr_resolution.html | 85 + .../app/src/main/assets/processor.html | 58 + .../main/assets/product_model_input_size.html | 62 + .../src/main/assets/product_resolution.html | 85 + .../assets/product_similaritythreshold.html | 11 + .../app/src/main/ic_launcher-playstore.png | Bin 0 -> 40054 bytes .../zebra/aidatacapturedemo/MainActivity.kt | 73 + .../aidatacapturedemo/PendoInitializer.kt | 13 + .../data/AIDataCaptureDemoUiState.kt | 210 ++ .../aidatacapturedemo/data/FilterData.kt | 83 + .../aidatacapturedemo/data/ModuleData.kt | 16 + .../aidatacapturedemo/data/ProductData.kt | 61 + .../aidatacapturedemo/data/ResultData.kt | 10 + .../model/BarcodeAnalyzer.kt | 279 +++ .../CustomClosedFloatingPointRangeAdapter.kt | 38 + .../aidatacapturedemo/model/FileUtils.kt | 454 ++++ .../aidatacapturedemo/model/FilterUtils.kt | 403 ++++ .../model/GenericEntityTrackerAnalyzer.kt | 253 ++ .../model/ProductEnrollmentRecognition.kt | 513 ++++ .../model/RetailShelfAnalyzer.kt | 131 + .../model/TextOCRAnalyzer.kt | 304 +++ .../zebra/aidatacapturedemo/ui/theme/Color.kt | 11 + .../zebra/aidatacapturedemo/ui/theme/Theme.kt | 50 + .../zebra/aidatacapturedemo/ui/theme/Type.kt | 21 + .../ui/view/AIDataCaptureDemoApp.kt | 510 ++++ .../AIDataCaptureModalNavigationDrawer.kt | 250 ++ .../ui/view/AIDataCaptureStartScreen.kt | 407 ++++ .../aidatacapturedemo/ui/view/AboutScreen.kt | 630 +++++ .../ui/view/BarcodeModelSettings.kt | 225 ++ .../ui/view/BulletHandler.kt | 32 + .../ui/view/CameraPreviewScreen.kt | 1393 +++++++++++ .../ui/view/CommonUIElements.kt | 721 ++++++ .../ui/view/CustomBulletSpan.kt | 79 + .../ui/view/CustomerInformationScreen.kt | 152 ++ .../ui/view/DemoSettingsScreen.kt | 710 ++++++ .../ui/view/DemoStartScreen.kt | 715 ++++++ .../ui/view/FeedbackUtils.kt | 137 ++ .../ui/view/GlobalConstants.kt | 362 +++ .../ui/view/NavigationStack.kt | 215 ++ .../ui/view/OCRBarcodeResultCapturedScreen.kt | 246 ++ .../ui/view/OCRBarcodeResultScreen.kt | 272 +++ .../ui/view/OCRModelSettings.kt | 441 ++++ .../ui/view/ProductsResultCapturedScreen.kt | 666 ++++++ .../ui/view/RetailModelSettings.kt | 185 ++ .../ui/view/SettingsMoreInfoScreen.kt | 426 ++++ .../ui/view/SingleResultScreen.kt | 355 +++ .../filters/BarcodeFindFilterHomeScreen.kt | 497 ++++ .../filters/CharacterMatchFilterScreen.kt | 567 +++++ .../view/filters/CharacterTypeFilterScreen.kt | 463 ++++ .../view/filters/OCRFindFilterHomeScreen.kt | 624 +++++ .../ui/view/filters/RegexFilterScreen.kt | 631 +++++ .../view/filters/StringLengthFilterScreen.kt | 317 +++ .../viewmodel/AIDataCaptureDemoViewModel.kt | 2127 +++++++++++++++++ .../src/main/res/drawable/barcode_icon.xml | 33 + .../app/src/main/res/drawable/camera_icon.xml | 17 + .../src/main/res/drawable/down_arrow_icon.xml | 13 + .../app/src/main/res/drawable/edit_icon.xml | 16 + .../src/main/res/drawable/flashlight_icon.xml | 12 + .../src/main/res/drawable/hamburger_icon.xml | 13 + .../drawable/ic_barcode_filter_selected.xml | 30 + .../app/src/main/res/drawable/ic_check.xml | 13 + .../src/main/res/drawable/ic_close_black.xml | 9 + .../main/res/drawable/ic_filter_default.xml | 13 + .../main/res/drawable/ic_filter_selected.xml | 18 + .../res/drawable/ic_launcher_background.xml | 74 + .../res/drawable/ic_launcher_foreground.xml | 51 + .../app/src/main/res/drawable/ic_location.xml | 13 + .../src/main/res/drawable/ic_menu_barcode.xml | 27 + .../app/src/main/res/drawable/ic_menu_ocr.xml | 29 + .../app/src/main/res/drawable/ic_next.xml | 9 + .../src/main/res/drawable/ic_nextsession.xml | 9 + .../res/drawable/ic_ocr_filter_selected.xml | 32 + .../app/src/main/res/drawable/ic_plus.xml | 9 + .../main/res/drawable/ic_previoussession.xml | 9 + .../main/res/drawable/ic_right_exapand.xml | 9 + .../app/src/main/res/drawable/ic_scan.xml | 9 + .../src/main/res/drawable/ic_trash_can.xml | 9 + .../app/src/main/res/drawable/icon_add.xml | 16 + .../main/res/drawable/icon_arrow_forward.xml | 9 + .../app/src/main/res/drawable/icon_close.xml | 9 + .../app/src/main/res/drawable/mic_icon.xml | 9 + .../src/main/res/drawable/ocr_finder_icon.xml | 43 + .../app/src/main/res/drawable/ocr_icon.xml | 29 + .../product_enrollment_recognition_icon.xml | 33 + .../main/res/drawable/retail_shelf_icon.xml | 24 + .../src/main/res/drawable/satisfied_icon.xml | 12 + .../src/main/res/drawable/settings_icon.xml | 13 + .../src/main/res/drawable/shutter_button.xml | 14 + .../src/main/res/drawable/technology_icon.xml | 13 + .../src/main/res/drawable/usecase_icon.xml | 18 + .../app/src/main/res/drawable/video_icon.xml | 13 + .../src/main/res/drawable/warning_icon.xml | 13 + .../src/main/res/drawable/zebra_logo_icon.xml | 12 + .../app/src/main/res/font/ibm_plex_sans.xml | 15 + .../src/main/res/font/ibm_plex_sans_bold.ttf | Bin 0 -> 218212 bytes .../main/res/font/ibm_plex_sans_medium.ttf | Bin 0 -> 218268 bytes .../main/res/font/ibm_plex_sans_regular.ttf | Bin 0 -> 218236 bytes .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 2482 bytes .../mipmap-hdpi/ic_launcher_background.webp | Bin 0 -> 44 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 1050 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 4288 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 1734 bytes .../mipmap-mdpi/ic_launcher_background.webp | Bin 0 -> 44 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 642 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 2670 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 3358 bytes .../mipmap-xhdpi/ic_launcher_background.webp | Bin 0 -> 46 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 1204 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 5724 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 5322 bytes .../mipmap-xxhdpi/ic_launcher_background.webp | Bin 0 -> 52 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 1738 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 8862 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 6914 bytes .../ic_launcher_background.webp | Bin 0 -> 52 bytes .../ic_launcher_foreground.webp | Bin 0 -> 2214 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 12448 bytes .../app/src/main/res/values/colors.xml | 10 + .../app/src/main/res/values/strings.xml | 224 ++ .../app/src/main/res/values/themes.xml | 4 + .../app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../Project 2/Project 2/build.gradle.kts | 6 + .../Project 2/Project 2/gradle.properties | 23 + .../Project 2/gradle/libs.versions.toml | 70 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + AISuite_Demos/Project 2/Project 2/gradlew | 185 ++ AISuite_Demos/Project 2/Project 2/gradlew.bat | 89 + .../Project 2/Project 2/settings.gradle.kts | 49 + 143 files changed, 19694 insertions(+), 4 deletions(-) create mode 100644 AISuite_Demos/Project 2/Project 2/.gitignore create mode 100644 AISuite_Demos/Project 2/Project 2/README.md create mode 100644 AISuite_Demos/Project 2/Project 2/app/.gitignore create mode 100644 AISuite_Demos/Project 2/Project 2/app/build.gradle.kts create mode 100644 AISuite_Demos/Project 2/Project 2/app/proguard-rules.pro create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/AndroidManifest.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/assets/barcode_model_input_size.html create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/assets/barcode_resolution.html create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/assets/barcode_symbologies.html create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/assets/ocr_model_input_size.html create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/assets/ocr_resolution.html create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/assets/processor.html create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/assets/product_model_input_size.html create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/assets/product_resolution.html create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/assets/product_similaritythreshold.html create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/ic_launcher-playstore.png create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/MainActivity.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/PendoInitializer.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/FilterData.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/ModuleData.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/ProductData.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/ResultData.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/BarcodeAnalyzer.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/CustomClosedFloatingPointRangeAdapter.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/FileUtils.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/FilterUtils.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/GenericEntityTrackerAnalyzer.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/ProductEnrollmentRecognition.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/RetailShelfAnalyzer.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/TextOCRAnalyzer.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Color.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Theme.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Type.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureDemoApp.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureModalNavigationDrawer.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureStartScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AboutScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeModelSettings.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BulletHandler.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CommonUIElements.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomBulletSpan.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoStartScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/FeedbackUtils.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultCapturedScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRModelSettings.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/ProductsResultCapturedScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/RetailModelSettings.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/SettingsMoreInfoScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/SingleResultScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/BarcodeFindFilterHomeScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/CharacterMatchFilterScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/CharacterTypeFilterScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/OCRFindFilterHomeScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/RegexFilterScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/StringLengthFilterScreen.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/barcode_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/camera_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/down_arrow_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/edit_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/flashlight_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/hamburger_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_barcode_filter_selected.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_check.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_close_black.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_filter_default.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_filter_selected.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_location.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_menu_barcode.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_menu_ocr.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_next.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_nextsession.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_ocr_filter_selected.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_plus.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_previoussession.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_right_exapand.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_scan.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_trash_can.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/icon_add.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/icon_arrow_forward.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/icon_close.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/mic_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ocr_finder_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ocr_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/product_enrollment_recognition_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/retail_shelf_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/satisfied_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/settings_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/shutter_button.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/technology_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/usecase_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/video_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/warning_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/zebra_logo_icon.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/font/ibm_plex_sans.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/font/ibm_plex_sans_bold.ttf create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/font/ibm_plex_sans_medium.ttf create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/font/ibm_plex_sans_regular.ttf create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/values/colors.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/values/strings.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/values/themes.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/xml/backup_rules.xml create mode 100644 AISuite_Demos/Project 2/Project 2/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 AISuite_Demos/Project 2/Project 2/build.gradle.kts create mode 100644 AISuite_Demos/Project 2/Project 2/gradle.properties create mode 100644 AISuite_Demos/Project 2/Project 2/gradle/libs.versions.toml create mode 100644 AISuite_Demos/Project 2/Project 2/gradle/wrapper/gradle-wrapper.jar create mode 100644 AISuite_Demos/Project 2/Project 2/gradle/wrapper/gradle-wrapper.properties create mode 100755 AISuite_Demos/Project 2/Project 2/gradlew create mode 100644 AISuite_Demos/Project 2/Project 2/gradlew.bat create mode 100644 AISuite_Demos/Project 2/Project 2/settings.gradle.kts diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt index 820e903..c7b29c8 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt @@ -185,7 +185,7 @@ private fun DrawAbstractBarcodeMapLayer( var currentLeftX = -1f - sortedRow.forEach { barcode -> + sortedRow.forEachIndexed { index, barcode -> val bBoxWidth = barcode.boundingBox.width().toFloat() var left = barcode.boundingBox.left.toFloat() @@ -203,7 +203,11 @@ private fun DrawAbstractBarcodeMapLayer( // Highlight if it's the selected tote val isTarget = uiState.selectedToteId == barcode.text + // Assign Tote Name A-F based on index + val toteName = if (index < 6) ('A' + index).toString() else "" + drawAbstractPickingUnit( + toteName = toteName, id = barcode.text, left = scaledLeft, top = scaledTop, @@ -220,6 +224,7 @@ private fun DrawAbstractBarcodeMapLayer( } private fun DrawScope.drawAbstractPickingUnit( + toteName: String, id: String, left: Float, top: Float, @@ -247,7 +252,7 @@ private fun DrawScope.drawAbstractPickingUnit( val paint = android.graphics.Paint().apply { this.color = if (isTarget) android.graphics.Color.WHITE else android.graphics.Color.BLACK - this.textSize = (if (isTarget) 12f else 9f) * density + this.textSize = (if (isTarget) 16f else 14f) * density // Increased font size this.textAlign = android.graphics.Paint.Align.CENTER this.isAntiAlias = true this.isFakeBoldText = true @@ -257,7 +262,8 @@ private fun DrawScope.drawAbstractPickingUnit( val textY = top + height / 2 - (paint.fontMetrics.ascent + paint.fontMetrics.descent) / 2 if (width > 25 * density) { - val displayId = if (id.length > 7) id.take(5) + ".." else id - drawContext.canvas.nativeCanvas.drawText(displayId, textX, textY, paint) + val last5Digits = if (id.length >= 5) id.takeLast(5) else id + val displayText = if (toteName.isNotEmpty()) "$toteName: $last5Digits" else last5Digits + drawContext.canvas.nativeCanvas.drawText(displayText, textX, textY, paint) } } diff --git a/AISuite_Demos/Project 2/Project 2/.gitignore b/AISuite_Demos/Project 2/Project 2/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/AISuite_Demos/Project 2/Project 2/README.md b/AISuite_Demos/Project 2/Project 2/README.md new file mode 100644 index 0000000..9f55c50 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/README.md @@ -0,0 +1,128 @@ +## AI Data Capture Demo Application + +This application demonstrates the features available in the Zebra AI Data Capture SDK - https://techdocs.zebra.com/ai-datacapture . The application demonstrates technology features, including **Barcode Recognizer**, **Text/OCR Recognizer**, and **Product & Shelf Recognizer**, and usecase feature including **Product & Shelf Enrollment**, and **OCR Barcode Finder**. Each feature is illustrated through a live preview that provides real-time, on-screen feedback, displaying bounding boxes around detected objects and additionally showing recognition results for Product and Shelf Recognition and OCR Text Find. + +## Project Purpose +Use this project as a sample for: +- Initializing and configuring Zebra's AI Data Capture SDK +- Processing camera frames with different foundational AI models +- Displaying real-time results in a Compose UI +- Managing state and business logic with MVVM + +## How It Works +1. **([CameraX](https://developer.android.com/media/camera/camerax)) Integration**: The app uses CameraX for camera lifecycle and frame analysis. +2. **EntityTrackerAnalyzer**: Camera frames are analyzed in real-time using Zebra's EntityTrackerAnalyzer, which detects and tracks barcodes. +3. **MVVM Architecture**: All SDK interactions are isolated in the "Model" layer/folder. ViewModels manage state and expose it to the UI via Kotlin Flows. +4. **Jetpack Compose UI**: UI layer/folder handles UI Screens. The UI observes ViewModel state and displays overlays which draw bounding boxes and results and handles screen transitions. + +## Useful References +- [SDK Documentation](https://techdocs.zebra.com/ai-datacapture/latest/about/) +- [Models](https://techdocs.zebra.com/ai-datacapture/latest/setup/#featuresmodels) +- [Developer Experience Videos](https://www.youtube.com/zebratechnologies) +- [Jetpack Compose Documentation](https://developer.android.com/jetpack/compose) +- [CameraX Documentation](https://developer.android.com/training/camerax) +- [Android Developer Documentation](https://developer.android.com/docs) + +## Getting Started + +### Prerequisites +- Android Studio Hedgehog or later +- Android SDK 34 (Android 14) +- Zebra AI Data Capture SDK ([Documentation](https://techdocs.zebra.com/enterprise-ai/vision-sdk/)) + +### Setup & Installation +1. **Clone the repository:** + ```bash + git clone + cd AIDataCaptureDemo + ``` +2. **Open in Android Studio:** + - Select "Open an Existing Project" and choose the project directory. +3. **Build the project:** + ```bash + ./gradlew build + ``` +4. **Run on device:** + - Connect an Android device with a camera and run the app from Android Studio. + +## Usage Overview +### Usecase Demos +**OCR Find + Barcode** - Optical Character and Barcode Recognition: +- Displays text recognition and barcode results on the live viewfinder. +- The filter icon enables the user to locate text by filtering for numeric, alphabetic, or alphanumeric content, including options for specifying size ranges or exact string matches +**Product & Shelf Enrollment** - Product Recognition: +- Enables creation of a product index +- Multi-step process required to create index followed displaying results on live viewfinder +- Recognizes products with results displayed as text within each product’s bounding box. +- Highlights enrolled products in green during enrollment; non-enrolled products do not display text results. + +#### Product & Shelf Enrollment Settings +Product & Shelf Enrollment is initialized to use products.db file in internal storage folder (filesDir). User has no direct access to this folder and file. +**Import Database** +- Allows user to select a database file using the file picker feature. +- This functionality is specifically intended for those managing a list of previously enrolled product database files. +- The feature allows users to load a product database file (*.db) from local storage. +- Once a file is selected, it is copied to the products.db file within the filesDir directory, + and Product Recognition is initialized to use the newly selected database file. + +**Clear Database** +- Clear’s database from file local storage. +- Shows “Deleted Product Database” toast + +**Export Database** +- Copies products.db file from filesDir, that is used currently for product recognition into **Downloads** folder. + +***Supporting Information*** +By default, this application does not include a product recognition database. However, users can manually enroll products using the _Product_ model setting sequence and save the associated database for future use. The application also allows users to load databases from memory. Product databases are located in the _/Download/_ directory, while manually labeled product images are stored in the _/Pictures/_ directory. + +Once manual labeling is complete, users can run real-time product recognition in _Product_ mode and review the results. If the product recognition results do not meet the desired accuracy levels, additional captures of the same product set may be needed. The sequence of screens required to perform product enrollment is shown below. + +### Technology Demos +**Text/OCR Recognizer** - Optical Character Recognition: +- Displays text recognition results on the live viewfinder. +- The settings menu offers controls to customize detection, recognition, and grouping features. + +#### Advanced OCR Settings +[Advanced OCR Settings](https://techdocs.zebra.com/ai-datacapture/latest/textocr/) allows developer to fine-tune performance for diverse use cases, including document scanning, real-time recognition, and automated data entry. + +**Barcode Recognizer** - Barcode Detection and Decode +- Displays a live camera preview. +- Highlights 1D and 2D barcodes with bounding boxes in various colors. +- Displays decoded barcode value below the bounding boxes. +**Product & Shelf Recognizer** - Product & Shelf Recognizer +- Provides a live camera preview with bounding boxes over detected features like shelves, labels, pegs labels and products. +- Provides solid bounding boxes and displays recognition results as text within each product’s bounding box. +- Uses specific colors for each feature: Red for shelves, Blue for labels, Magenta for Peg and Green for products. + +### Generic Settings - Model Processing Configurations +**CPU / GPU / DSP** - Configures processor type for running the selected model +- CPU – runs model on CPU, this is typically the least performant for inference time +- GPU – runs model on GPU, typically higher performance than CPU +- DSP – runs model on DSP, fastest performance + +**640 / 1280 / 1600 / 2560** - Configures the AI Models to run at a specific input sizes +- 640 -> set’s the model inference dimensions to 640x640 +- 1280 -> set’s the model inference dimensions to 1280x1280 +- 1600 -> set’s the model inference dimensions to 1600x1600 +- 2560 -> set’s the model inference dimensions to 2560x2560 + +**1MP / 2MP / 4MP / 8MP** - Configures the AI Models to run at a specific input image resolution +- 1MP -> set’s the analyzer input image resolution to 1280x720 +- 2MP -> set’s the analyzer input image resolution to 1920x1080 +- 4MP -> set’s the analyzer input image resolution to 2688x1512 +- 8MP -> set’s the analyzer input image resolution to 3840x2160 + +Typically lower resolutions should be used when capturing images close-up while higher resolutions allow detection of smaller features or features that are far away. + +### Build Dependencies: +This application requires specific dependencies made available by Zebra through a maven repository. Access to this repository is necessary in order for the application to include all the required libraries required. +## Support +If you encounter any issues or have questions about using the AI Suite, feel free to contact Zebra Technologies support through the official support page. + +## Thank You +Lastly, thank you for being a part of our community. If you have any quesitons, please reach out to our DevRel team at developer@zebra.com + +This README.md is designed to provide clarity and a user-friendly onboarding experience for developers. If you have specific details about the project that you would like to include, feel free to let us know! + +## License +All content under this repository's root folder is subject to the [Development Tool License Agreement](../../Zebra%20Development%20Tool%20License.pdf). By accessing, using, or distributing any part of this content, you agree to comply with the terms of the Development Tool License Agreement. diff --git a/AISuite_Demos/Project 2/Project 2/app/.gitignore b/AISuite_Demos/Project 2/Project 2/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/build.gradle.kts b/AISuite_Demos/Project 2/Project 2/app/build.gradle.kts new file mode 100644 index 0000000..c618a96 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/build.gradle.kts @@ -0,0 +1,133 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.zebra.aidatacapturedemo" + compileSdk = 36 + + val appVersion: String = libs.versions.appVersion.get().toString() + + androidResources { + noCompress.add("tar") + noCompress.add("tar.crypt") + } + + defaultConfig { + applicationId = "com.zebra.aidatacapturedemo" + minSdk = 33 + targetSdk = 36 + versionCode = 24 + versionName = appVersion + + buildConfigField("String", "AI_DataCaptureDemo_Version", "\"$appVersion\"") + + val zebraAIVisionSdk: String = libs.versions.zebraAIVisionSdk.get().toString() + buildConfigField("String", "Zebra_AI_VisionSdk_Version", "\"$zebraAIVisionSdk\"") + + val barcodeLocalizer: String = libs.versions.barcodeLocalizer.get().toString() + buildConfigField("String", "BarcodeLocalizer_Version", "\"$barcodeLocalizer\"") + + val textOcrRecognizer: String = libs.versions.textOcrRecognizer.get().toString() + buildConfigField("String", "TextOcrRecognizer_Version", "\"$textOcrRecognizer\"") + + val productAndShelfRecognizer: String = libs.versions.productAndShelfRecognizer.get().toString() + buildConfigField("String", "ProductAndShelfRecognizer_Version", "\"$productAndShelfRecognizer\"") + + val pendoApiKey = System.getenv("aidatacapturedemo_pendo_api_key") ?: "" + buildConfigField(type = "String", name = "PendoApiKey", value = "\"$pendoApiKey\"") + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + applicationVariants.all { + outputs.all { + val output = this as com.android.build.gradle.internal.api.BaseVariantOutputImpl + // APK Output filename sample: AIDataCaptureDemo1.0.0.apk + output.outputFileName = "AIDataCaptureDemo${appVersion}.apk" + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + buildFeatures { + viewBinding = true + buildConfig = true + compose = true + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + isCoreLibraryDesugaringEnabled = true + } + kotlinOptions { + jvmTarget = "1.8" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + jniLibs { + useLegacyPackaging = true + } + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.constraintlayout) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.play.services.tasks) + implementation(libs.androidx.navigation.compose.android) + implementation(libs.androidx.documentfile) + coreLibraryDesugaring(libs.desugar.jdk.libs) + + implementation(libs.androidx.navigation.compose) + implementation(libs.coil.compose) + implementation(libs.jsoup) + + implementation(libs.camera.core) + implementation(libs.camera.camera2) + implementation(libs.camera.lifecycle) + implementation(libs.camera.view) + implementation(libs.androidx.camera.extensions) + implementation(libs.runtime.permissions) + + // JSON serialization + implementation(libs.gson) + + //Below dependency is to get AI Suite SDK + implementation(libs.zebra.ai.vision.sdk) { artifact { type = "aar" } } + + //Below dependency is to get Barcode Localizer model for AI Suite SDK + implementation(libs.barcode.localizer) { artifact { type = "aar" } } + + //Below dependency is to get OCR model for AI Suite SDK + implementation(libs.text.ocr.recognizer) { artifact { type = "aar" } } + + //Below dependency is to get product Recognition model for AI Suite SDK + implementation(libs.product.and.shelf.recognizer) { artifact { type = "aar" } } + + androidTestImplementation(platform(libs.androidx.compose.bom)) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + // Pendo SDK + implementation(libs.pendo.io) { isChanging = true } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/proguard-rules.pro b/AISuite_Demos/Project 2/Project 2/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/AndroidManifest.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..11cbeae --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/AndroidManifest.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/assets/barcode_model_input_size.html b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/barcode_model_input_size.html new file mode 100644 index 0000000..8d17ea2 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/barcode_model_input_size.html @@ -0,0 +1,70 @@ +
+ Model input size is the resolution your image is resized to before AI + analysis. + Smaller sizes are faster and use less memory, while larger sizes can help + detect smaller or more distant barcodes —but also uses more + processing power and memory. + Choose the input size to balance speed and accuracy for your needs. + Note: Model Input Size can be customized in increments of 32 + using the SDK. The options below represent some generic sizes. +
+
+ Model input size is the resolution your image is resized to before AI + analysis. + Smaller sizes are faster, while larger sizes improve accuracy. + Choose the input size to balance speed and accuracy for your needs. + Note: Model Input Size can be customized in increments of 32 + using the SDK. The options below represent some generic sizes. +
+
Model Input Size Options
+
+

Small – 640 x 640

+
    +
  • Fastest processing
  • +
  • Best for large, clear, or close-up barcodes
  • +
  • May miss small, damaged, or distant barcodes
  • +
+

Medium – 1280 x 1280

+
    +
  • Balanced speed and accuracy
  • +
  • Handles most barcode types and sizes in standard conditions
  • +
+

Large – 1600 x 1600

+
    +
  • Higher accuracy
  • +
  • Best for small, damaged, or distant barcodes,  
  • +
  • Slower Processing, and highest memory/battery consumption
  • +
  • Not recommended for CPU/GPU (inference type)
  • +
+
+
+

+ Recommendation:
+

+
    +
  • Start with 640x640 for close/large barcode.
  • +
  • Increase to the next size if you encounter issues reading barcodes.
  • +
  • + Use + larger model input sizes only for the most demanding and challenging + use cases, and only if your device has sufficient processing power and memory. +
  • +
+
+
+

+ Tip: Larger model input sizes improve accuracy but come at a + significant cost to speed, memory, and battery life. If the app becomes slow + or unstable, reduce the input size. + Experiment to find the smallest size that works reliably for your use + case. +

+
\ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/assets/barcode_resolution.html b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/barcode_resolution.html new file mode 100644 index 0000000..435f89a --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/barcode_resolution.html @@ -0,0 +1,85 @@ +
+ Camera resolution is the number of pixels in your photo (e.g., 1MP = + 1280x720). + Higher resolution captures more detail for small or distant barcodes but + uses more power and memory. + Benefits are limited if the model input size is low. +
+
+ The camera resolution is the number of pixels in the image captured by your + device (e.g., 1MP = 1280x720, 8MP = 3840x2160). This affects the detail in + the original photo before it’s resized for AI processing. + Higher resolution means more detail, helping read small, or distant + barcodes—but also uses more processing power and memory. + If speed, battery life, or memory use are your top priorities, low resolution + can be a good choice for large or close-up barcodes. + Note: If the model input size is set low, using a high + camera resolution won’t improve results much, since the detailed image + will be downscaled before processing. +
+
Resolution Options
+
+

1MP (1280 x 720)

+
    +
  • Fastest, power-efficient
  • +
  • Best for simple, large, or close-up barcodes
  • +
  • May miss small or damaged barcodes
  • +
+

2MP (1920 x 1080)

+
    +
  • Good for general use, moderate detail
  • +
  • Handles most standard barcode scanning scenarios
  • +
+

4MP (2688 x 1512)

+
    +
  • + Captures more detail, good for poor contrast or dense, smaller barcodes +
  • +
  • Higher memory and battery use
  • +
+

8MP (3840 x 2160)

+
    +
  • Maximum detail and accuracy,
  • +
  • Best for extremely small, dense, or challenging use cases.
  • +
  • Slowest and highest memory/battery consumption
  • +
  • + Limited benefit if model input size is low; Not recommended for CPU/GPU + (inference types) +
  • +
+
+
+

Recommendation:

+
    +
  • + Start with 1 or 2MP. This offers a good balance between + image detail, speed, and battery use. +
  • +
  • + Increase to 4MP or 8MP only if you + need to capture very small, faint, or distant barcodes, and your device + can handle the extra processing. +
  • +
  • + Match your camera resolution to your model input size: If + your model input size is 640x640, higher resolutions provide little + benefit. For larger model input sizes (like 1280x1280 or 1600x1600), a + higher resolution can help. +
  • +
+
+
+

+ Tip: Make sure the barcode(s) you want to scan appears + at least 8 pixels tall in your image for reliable + recognition. + Experiment to find the smallest resolution that works reliably for your + use case +

+
diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/assets/barcode_symbologies.html b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/barcode_symbologies.html new file mode 100644 index 0000000..28d6dfa --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/barcode_symbologies.html @@ -0,0 +1,53 @@ +
+ Select the types of barcodes to be recognized. + Enabling more symbologies may slow down scanning and increase power consumption. + Choose the symbologies based on your barcode scanning requirements. + Note: Different symbology types are optimized for different use cases and + industries. +
+
+ Select the types of barcodes to be recognized. + Enabling more symbologies may slow down scanning and increase power consumption. + Choose the symbologies based on your barcode scanning requirements. + Note: Different symbology types are optimized for different use cases and + industries. +
+
Barcode Symbology Types
+
+

1D Barcodes

+
    +
  • Linear barcodes like Code 39, Code 128, UPC, EAN
  • +
  • Widely used in retail and manufacturing
  • +
  • Fast scanning but limited data capacity
  • +
+

2D Barcodes

+
    +
  • Matrix codes like QR Code, DataMatrix, PDF417
  • +
  • Higher data density and error correction
  • +
  • Can store more information including text, URLs, and binary data
  • +
+

GS1 Barcodes

+
    +
  • Global standards for supply chain and retail
  • +
  • Includes GS1 DataBar, GS1 DataMatrix, GS1 QR Code
  • +
  • Used for product identification and traceability
  • +
+

Postal Barcodes

+
    +
  • Specialized for mail and package tracking
  • +
  • Country-specific formats
  • +
  • Optimized for automated sorting systems
  • +
+
+
+

+ Recommendation:
+

+
    +
  • Choose the symbologies based on your barcode scanning requirements.
  • +
+
\ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/assets/ocr_model_input_size.html b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/ocr_model_input_size.html new file mode 100644 index 0000000..0d46724 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/ocr_model_input_size.html @@ -0,0 +1,60 @@ +
+ Model input size is the resolution your image is resized to before AI + analysis. + Smaller sizes are faster, while larger sizes improve accuracy. + Choose the input size to balance speed and accuracy for your needs. + Note: Model Input Size can be customized in increments of 32 + using the SDK. The options below represent some generic sizes. +
+
+ Model input size is the resolution your image is resized to before AI + analysis. + Smaller sizes are faster, while larger sizes improve accuracy. + Choose the input size to balance speed and accuracy for your needs. + Note: Model Input Size can be customized in increments of 32 + using the SDK. The options below represent some generic sizes. +
+
Model Input Size Options
+
+

Small – 640 x 640

+
    +
  • Fastest processing
  • +
  • Best for large or close-up text
  • +
  • May miss small/fine details
  • +
+

Medium – 1280 x 1280

+
    +
  • Balanced speed and accuracy
  • +
  • Good for moderately small text or stylized fonts and handwriting
  • +
+

Large – 1600 x 1600

+
    +
  • Higher accuracy
  • +
  • Best for small, distant or poor contrast text
  • +
  • Slower, uses more memory
  • +
  • Not recommended for CPU/GPU (inference type)
  • +
+

Extra-large – 2560 x 2560

+
    +
  • Maximum detail and accuracy
  • +
  • Useful for highly challenging recognition tasks
  • +
  • Slowest processing and highest memory/battery consumption
  • +
  • Not recommended for CPU/GPU (inference type)
  • +
+
+
+

Recommendation:

+
    +
  • Start with 640x640
  • +
  • Increase to 1280x1280 when 640x640 misses small, faint, or low-contrast text.
  • +
  • Use 1600x1600 for very small or distant text, dense documents, or when maximum detail is needed. Caution: this can slow processing and use more battery.
  • +
  • Use 2560x2560 only for the most demanding and challenging use cases, and only if your device has sufficient processing power and memory.
  • +
+
+
+

Tip: Larger model input sizes improve accuracy but come at a significant cost to speed, memory, and battery life. If the app becomes slow or unstable, reduce the input size. Experiment to find the smallest size that works reliably for your use case.

+
diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/assets/ocr_resolution.html b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/ocr_resolution.html new file mode 100644 index 0000000..e35303a --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/ocr_resolution.html @@ -0,0 +1,85 @@ +
+ Camera resolution is the number of pixels in your photo (e.g., 1MP = + 1280x720). + Higher resolution captures more detail for small or distant text but uses + more power and memory. + Benefits are limited if the model input size is low. +
+
+

+ The camera resolution is the number of pixels in the image captured by your + device (e.g., 1MP = 1280x720, 8MP = 3840x2160). This affects the detail in + the original photo before it’s resized for AI processing. + Higher resolution means more detail, helping read small, faint, or + distant text—but also uses more processing power and memory. If + speed, battery life, or memory use are your top priorities, low resolution + can be a good choice for large or close-up text. Note: + If the model input size is set low, using a high camera resolution + won’t improve results much, since the detailed image will be downscaled + before processing. +

+
+
Resolution Options
+
+

1MP (1280 x 720)

+
    +
  • Fastest, power-efficient
  • +
  • Best for simple/large text
  • +
  • May miss small/fine print
  • +
+

2MP(1920 x 1080)

+
    +
  • Good for general use, moderate detail
  • +
  • Handles most basic OCR needs
  • +
+

4MP (2688 x 1512)

+
    +
  • Captures more detail, good for dense text or forms
  • +
  • Higher memory and battery use
  • +
+

8MP (3840 x 2160)

+
    +
  • Maximum detail and accuracy,
  • +
  • Best for extremely small, dense, or faint text
  • +
  • Slowest and highest memory/battery consumption
  • +
  • + Limited benefit if model input size is low; + Not recommended for CPU/GPU (inference types) +
  • +
+
+
+

Recommendation:

+
    +
  • + Start with 2MP (1920x1080) resolution. This offers a good + balance between image detail, speed, and battery use. +
  • +
  • + Increase to 4MP or 8MP only if you need to capture very + small, faint, or distant text, and your device can handle the extra + processing. +
  • +
  • + Match your camera resolution to your model input size: If + your model input size is 640x640, higher resolutions provide little + benefit. For larger model input sizes (like 1280x1280 or 1600x1600), a + higher resolution can help. +
  • +
+
+
+

+ Tip: Make sure the text you want to read appears + at least 16 pixels tall in your image for reliable + recognition. + Experiment to find the smallest resolution that works reliably for your + use case +

+
diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/assets/processor.html b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/processor.html new file mode 100644 index 0000000..bff9067 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/processor.html @@ -0,0 +1,58 @@ +
+ This setting chooses which chip in your device runs AI tasks, affecting speed + and battery life. Not all devices have a DSP. +
+
+

+ This setting chooses which chip in your device runs AI tasks, affecting speed + and battery life.
Note: These models are designed to + work the best with devices that have a DSP, on IOT Platforms, chipsets + without a DSP will be slower. for details on compatible models, look for + Zebra QC6490 and QC4490 mobile computers. see Zebra Platform Devices +

+
+
Inference (processor) Types
+
+

DSP (Digital Signal Processor)

+
    +
  • Recommended: Fastest and most battery-efficient
  • +
  • Ideal for real-time, energy-efficient tasks
  • +
  • + Note: DSP is only supported in specific chipsets and not + available in all devices +
  • +
+

+ GPU (Graphics Processing Unit)
For trial use, may be acceptable in some lightweight applications +

+
    +
  • Slower than DSP
  • +
  • Uses more power than DSP
  • +
  • Use if DSP is not available
  • +
  • Use if DSP is not available, also consider trialling CPU
  • +
+

+ CPU (Central Processing Unit)
For trial use, may be acceptable in some lightweight applications +

+
    +
  • Always available
  • +
  • Slower than DSP
  • +
  • Use if DSP is not available, also consider trialling GPU
  • +
+
+
+

Recommendation:

+
    +
  • Always use DSP if available on your device
  • +
+
+
+

+ Tip: Choosing the right processor improves speed and + battery life, especially during continuous or real-time use. +

+
diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/assets/product_model_input_size.html b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/product_model_input_size.html new file mode 100644 index 0000000..ec902a4 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/product_model_input_size.html @@ -0,0 +1,62 @@ +
+ Model input size is the resolution your image is resized to before AI + analysis. Smaller sizes are faster and use less memory, while larger sizes can help + detect smaller or more distant labels and products—but also uses + more processing power and memory. Choose the input size to balance speed and accuracy for your needs. + Note: Model Input Size can be customized in increments of 32 + using the SDK. The options below represent some generic sizes. +
+
+ Model input size is the resolution your image is resized to before AI + analysis. Smaller sizes are faster and use less memory, while larger sizes can help + detect smaller or more distant labels and products—but also uses + more processing power and memory. Choose the input size to balance speed and accuracy for your needs. + Note: Model Input Size can be customized in increments of 32 + using the SDK. The options below represent some generic sizes. +
+
Model Input Size Options
+
+

Small – 640 x 640

+
    +
  • Fastest processing
  • +
  • Best for large format products and labels where products, label font, and barcode lack fine details.
  • +
  • Small labels and products may not be detected particularly if the shelf is captured far away.
  • +
+

Medium – 1280 x 1280

+
    +
  • Balanced speed and accuracy
  • +
  • Ideal for retail item and label detection, may have better coverage compared to lower resolutions at the same distance.
  • +
+

Large – 1600 x 1600

+
    +
  • Higher accuracy and detection coverage for smaller labels, products and slim shelves. Improved recognition accuracy for products with smaller, more dense details.
  • +
  • Useful for challenging environments where the mobile user may have to capture shelf from a further than typical distance or product size is small and item density on shelf has increased.
  • +
  • Slower, uses more memory, processing, and battery
  • +
  • Not recommended for CPU/GPU (inference type)
  • +
+

Extra-large – 2560 x 2560

+
    +
  • Maximum detail and accuracy,
  • +
  • Best for use cases where coverage on small items and labels are a challenge or where images are taken far away from the shelf edge.
  • +
  • Ideal for highly challenging use cases where products have fine details.
  • +
  • Slowest processing and highest memory/battery consumption
  • +
  • Not recommended for CPU/GPU (inference type)
  • +
+
+
+

Recommendation:

+
    +
  • Start with 1280x1280 for typical shelf, label and product samples.
  • +
  • Decrease to 640x640 if products and labels are reasonably sized and the application requires less latency and improved battery life.
  • +
  • Increase to 1600x1600 if you encounter issues detecting small labels or small products.
  • +
  • Use 2560 x 2560 for situations where product size is a challenge or user is unable to get close to the shelf edge or product details are extremely fine. Caution: this can slow processing and adversely impact battery life,
  • +
  • Use 2560x2560 only for the most demanding and only if your device has sufficient processing power and memory.
  • +
+
+
+

Tip: Larger model input sizes improve accuracy but come at a significant cost to speed, memory, and battery life. If the app becomes slow or unstable, reduce the input size. Experiment to find the smallest size that works reliably for your use case.

+
diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/assets/product_resolution.html b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/product_resolution.html new file mode 100644 index 0000000..2c57290 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/product_resolution.html @@ -0,0 +1,85 @@ +
+ Camera resolution is the number of pixels in your photo (e.g., 1MP = + 1280x720). + Higher resolution captures more detail for small or distant text but uses + more power and memory. + Benefits are limited if the model input size is low. +
+
+ The camera resolution is the number of pixels in the image captured by your + device (e.g., 1MP = 1280x720). This affects the detail in the original photo + before it’s resized for AI processing. + Higher resolution means more detail, helping read small, faint, or + distant text—but also uses more processing power and memory. If + speed, battery life, or memory use are your top priorities, low resolution + can be a good choice for large or close-up text. Note: + If the model input size is set low, using a high camera resolution + won’t improve results much, since the detailed image will be downscaled + before processing. +
+
Resolution Options
+
+

1MP (1280 x 720)

+
    +
  • Fastest, power-efficient
  • +
  • Best for large products at close distance
  • +
  • May miss product details at long distance
  • +
+

2MP (1920 x1 080)

+
    +
  • Good for general use at moderate distance
  • +
  • Handles most product detection needs
  • +
+

4MP (2688 x 1512)

+
    +
  • + Captures more detail, good for dense shelves of product at longer distance +
  • +
  • Higher memory and battery use
  • +
+

8M(3840 x 2160)

+
    +
  • Maximum detail and accuracy,
  • +
  • Best for extremely small, dense, or faint text
  • +
  • Slowest and highest memory/battery consumption
  • +
  • + Limited benefit if model input size is low; Not recommended for CPU/GPU + (inference types) +
  • +
+
+
+

Recommendation:

+
    +
  • + Start with 2MP (1920x1080) resolution. This offers a good + balance between image detail, speed, and battery use. +
  • +
  • + Increase to 4MP if you need to capture very small, faint, + distant product details, and your device can handle the extra processing. +
  • +
  • + Match your camera resolution to your model input size: If + your model input size is 640x640, higher resolutions provide little + benefit. For larger model input sizes (like 1280x1280 or 1600x1600), a + higher resolution can help. +
  • +
+
+
+

+ Tip: Make sure the resolution matches the increased level of + product distinction required for your use case. is This is essential for + distinguishing visually similar products, as it allows the model to capture + details that low-resolution images might miss. + Experiment to find the smallest resolution that works reliably for your + use case +

+
diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/assets/product_similaritythreshold.html b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/product_similaritythreshold.html new file mode 100644 index 0000000..776cc4b --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/assets/product_similaritythreshold.html @@ -0,0 +1,11 @@ +
+ Similarity is a measure of the likeness or how alike two objects are, + as determined by a computer vision system +
+
+ Set the minimum level of confidence used to detect the similarity of + a product compared to products that are enrolled where a higher percentage + indicates a greater degree of similarity.

+ Minimum Similarity Level
+ Higher percentage results in greater confidence of an accurate match. +
\ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/ic_launcher-playstore.png b/AISuite_Demos/Project 2/Project 2/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..4a7b6830964dac183835ea20451d2c2304c69fe8 GIT binary patch literal 40054 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
  • tags and apply BulletSpan for list items. + */ +class BulletHandler : Html.TagHandler { + class Bullet + + override fun handleTag(opening: Boolean, tag: String, output: Editable, xmlReader: XMLReader) { + if (tag == "li" && opening) { + output.setSpan(Bullet(), output.length, output.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + } + if (tag == "li" && !opening) { + output.append("\n\n") + val lastMark = output.getSpans(0, output.length, Bullet::class.java).lastOrNull() + lastMark?.let { + val start = output.getSpanStart(it) + output.removeSpan(it) + if (start != output.length) { + output.setSpan(BulletSpan(), start, output.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + } + } + } + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt new file mode 100644 index 0000000..191273b --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt @@ -0,0 +1,1393 @@ +package com.zebra.aidatacapturedemo.ui.view + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.Log +import android.util.Size +import android.view.WindowManager +import android.view.WindowMetrics +import androidx.activity.compose.BackHandler +import androidx.camera.view.PreviewView +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.getSystemService +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.viewModelScope +import androidx.navigation.NavController +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.AdvancedFilterOption +import com.zebra.aidatacapturedemo.data.CharacterMatchFilterOption +import com.zebra.aidatacapturedemo.data.OcrRegularFilterOption +import com.zebra.aidatacapturedemo.data.UsecaseState +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import kotlinx.coroutines.launch +import kotlin.math.min + +/** + * This composable function represents the Camera Preview Screen in the AI Data Capture Demo app. + * It displays the camera feed and overlays results such as OCR, Barcode, Retail Module detection, + * on top of it. + * + * @param viewModel The ViewModel that holds the UI state and business logic for the screen. + * @param navController The NavController used for navigation between screens. + * @param context The Context of the current state of the application. + * @param activityInnerPadding The padding values to account for system UI elements like status bar and navigation bar. + * @param activityLifecycle The Lifecycle of the activity to manage camera resources appropriately. + */ +private const val TAG = "CameraPreviewScreen" +@Composable +fun CameraPreviewScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + context: Context, + activityInnerPadding: PaddingValues, + activityLifecycle: Lifecycle +) { + val uiState = viewModel.uiState.collectAsState().value + val lifecycleOwner = LocalLifecycleOwner.current + var showInfo = remember { mutableStateOf(true) } + val analysisUseCaseCameraResolution = when (viewModel.getSelectedResolution()) { + 0 -> Size(1280, 720) + 1 -> Size(1920, 1080) + 2 -> Size(2688, 1512) + 3 -> Size(3840, 2160) + else -> Size(1920, 1080) + } + + // GET DEVICE RESOLUTION: + val displayMetrics = LocalContext.current.resources.displayMetrics + val displayMetricsDensity = displayMetrics.density + + val windowManager = getSystemService(context, WindowManager::class.java) + val windowMetrics: WindowMetrics = windowManager.currentWindowMetrics + + val displayTotalWidthInPx = windowMetrics.bounds.width() + val displayTotalHeightInPx = windowMetrics.bounds.height() + + // TOP STATUS BAR + val displayStatusBarPaddingValues = WindowInsets.statusBars.asPaddingValues() + val displayStatusBarHeightInDp = displayStatusBarPaddingValues.calculateTopPadding() + val displayStatusBarHeightInPx = displayStatusBarHeightInDp.value * displayMetricsDensity + + // BOTTOM NAVIGATION BAR + val displayNavigationBarPaddingValues = WindowInsets.navigationBars.asPaddingValues() + val displayNavigationBarHeightInDp = displayNavigationBarPaddingValues.calculateBottomPadding() + val displayNavigationBarHeightInPx = + displayNavigationBarHeightInDp.value * displayMetricsDensity + + val availableHeightInPx = + displayTotalHeightInPx.toFloat() - displayStatusBarHeightInPx - displayNavigationBarHeightInPx + + // The following computed values are used for drawing Bbox overlay on the preview + val scaler = min( + displayTotalWidthInPx.toFloat() / analysisUseCaseCameraResolution.height.toFloat(), + availableHeightInPx / analysisUseCaseCameraResolution.width.toFloat() + ) + val scaledWidth = scaler * analysisUseCaseCameraResolution.height.toFloat() + val scaledHeight = scaler * analysisUseCaseCameraResolution.width.toFloat() + val gapX = (displayTotalWidthInPx - scaledWidth) / 2f + val gapY = (availableHeightInPx - scaledHeight) / 2f + + val previewView = remember { PreviewView(context) } + + // Navigation for Picking Flow + LaunchedEffect(uiState.pickingFeedback) { + if (uiState.pickingFeedback?.startsWith("item identified") == true) { + navController.navigate(Screen.BarcodeMapPicking.route) + } + } + + LaunchedEffect(key1 = "clear all the previous results") { + // clear all the previous results during Fresh Launch + when (uiState.usecaseSelected) { + UsecaseState.OCRBarcodeFind.value -> { + viewModel.updateOcrResultData(results = null) + viewModel.updateBarcodeResultData(results = listOf()) + } + + UsecaseState.OCR.value -> { + viewModel.updateOcrResultData(results = null) + } + + UsecaseState.Barcode.value, + UsecaseState.BarcodeMap.value -> { + viewModel.updateBarcodeResultData(results = listOf()) + } + + UsecaseState.Retail.value -> { + viewModel.updateRetailShelfDetectionResult(results = null) + } + + UsecaseState.Product.value -> { + viewModel.updateRetailShelfDetectionResult(results = null) + viewModel.updateProductRecognitionResult(results = null) + } + } + viewModel.setZoom(1.0f) + } + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + + LaunchedEffect(lifecycleOwner) { + viewModel.setupCameraController( + previewView = previewView, + analysisUseCaseCameraResolution = analysisUseCaseCameraResolution, + lifecycleOwner = lifecycleOwner, + activityLifecycle = activityLifecycle + ) + } + + previewView.scaleType = PreviewView.ScaleType.FIT_CENTER + Box( // Bottom layer + modifier = Modifier + .fillMaxSize() + .padding( + top = displayStatusBarHeightInDp, + bottom = displayNavigationBarHeightInDp + ) + ) { + AndroidView( // 2 layer + { previewView } + ) + + when (val selectedDemo = uiState.usecaseSelected) { + UsecaseState.OCRBarcodeFind.value -> { + + if (uiState.isBarcodeModelEnabled) { + + // For Barcode results during CHARACTER_MATCH filter: Play beep (or) vibrate when filtered results are found. + if (uiState.barcodeFilterData.selectedAdvancedFilterOptionList.contains( + AdvancedFilterOption.CHARACTER_MATCH + ) + ) { + if (uiState.barcodeFilterData.selectedCharacterMatchFilterData.startsWithStringList.isNotEmpty() || + uiState.barcodeFilterData.selectedCharacterMatchFilterData.containsStringList.isNotEmpty() || + uiState.barcodeFilterData.selectedCharacterMatchFilterData.exactMatchStringList.isNotEmpty() + ) { + if (uiState.barcodeResults.isNotEmpty()) { + if (uiState.ocrBarcodeFindSettings.feedbackSettings.audioBeep) { + FeedbackUtils.beep() + } + if (uiState.ocrBarcodeFindSettings.feedbackSettings.vibration) { + FeedbackUtils.vibrate() + } + } + } + } + + DrawBarcodeResult( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + } + + if (uiState.isOCRModelEnabled) { + when (uiState.ocrFilterData.selectedRegularFilterOption) { + OcrRegularFilterOption.UNFILTERED -> { + DrawOCRResult( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + } + + OcrRegularFilterOption.REGEX -> { + val checkIconDrawable = ContextCompat.getDrawable( + context, + R.drawable.ic_check + ) + DrawOCRResultWithTextSizeScalingAndCheckMark( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity, + checkIconDrawable = checkIconDrawable, + displayTotalHeightInPx = displayTotalHeightInPx, + displayTotalWidthInPx = displayTotalWidthInPx + ) + if (uiState.ocrResults.isNotEmpty()) { + if (uiState.ocrBarcodeFindSettings.feedbackSettings.audioBeep) { + FeedbackUtils.beep() + } + if (uiState.ocrBarcodeFindSettings.feedbackSettings.vibration) { + FeedbackUtils.vibrate() + } + } + } + + OcrRegularFilterOption.ADVANCED -> { + if (uiState.ocrFilterData.selectedAdvancedFilterOptionList.contains( + AdvancedFilterOption.CHARACTER_MATCH + ) + ) { + if (uiState.ocrFilterData.selectedCharacterMatchFilterData.startsWithStringList.isNotEmpty() || + uiState.ocrFilterData.selectedCharacterMatchFilterData.containsStringList.isNotEmpty() || + uiState.ocrFilterData.selectedCharacterMatchFilterData.exactMatchStringList.isNotEmpty() + ) { + val checkIconDrawable = ContextCompat.getDrawable( + context, + R.drawable.ic_check + ) + DrawOCRResultWithTextSizeScalingAndCheckMark( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity, + checkIconDrawable = checkIconDrawable, + displayTotalHeightInPx = displayTotalHeightInPx, + displayTotalWidthInPx = displayTotalWidthInPx + ) + if (uiState.ocrResults.isNotEmpty()) { + if (uiState.ocrBarcodeFindSettings.feedbackSettings.audioBeep) { + FeedbackUtils.beep() + } + if (uiState.ocrBarcodeFindSettings.feedbackSettings.vibration) { + FeedbackUtils.vibrate() + } + } + } + + if (uiState.ocrFilterData.selectedCharacterMatchFilterData.type == CharacterMatchFilterOption.EXACT_MATCH && uiState.isCaptureOrLiveEnabled == 1) { + showInformationBox( + info = "Looking for: ${uiState.ocrFilterData.selectedCharacterMatchFilterData.exactMatchStringList}", + topPadding = activityInnerPadding.calculateTopPadding() + displayStatusBarHeightInDp + ) + } + + } else { + DrawOCRResult( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + } + } + } + } + } + + UsecaseState.OCR.value -> { + + DrawOCRResult( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + } + + UsecaseState.Barcode.value, + UsecaseState.BarcodeMap.value -> { + DrawBarcodeResult( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + } + + UsecaseState.Retail.value -> { + DrawModuleRecognitionResult( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + } + + UsecaseState.Product.value -> { + DrawRetailShelfResult( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + DrawProductRecognitionResult( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY + ) + + if (showInfo.value && uiState.cameraError == null) { + HandleTopInfo( + icon = R.drawable.camera_icon, + info = stringResource(R.string.instruction_1), + showInfo = showInfo + ) + } + } + + UsecaseState.Main.value -> { + + } + + else -> { + TODO("Unhandled usecaseState received = $selectedDemo") + } + } + + // Show Picking Feedback overlay + if (uiState.selectedCustomer != null && uiState.pickingFeedback != null) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 100.dp), + contentAlignment = Alignment.TopCenter + ) { + Text( + text = uiState.pickingFeedback ?: "", + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ), + modifier = Modifier + .background( + if (uiState.pickingFeedback?.contains("incorrect") == true) Color.Red else Color(0xFF006D39), + RoundedCornerShape(8.dp) + ) + .padding(16.dp) + ) + } + } + } + + uiState.cameraError?.let { + showCameraErrorText() + } + + if (uiState.isCameraReady) { + showBottomBar( + navController = navController, + viewModel = viewModel, + activityInnerPadding = activityInnerPadding + ) + } +} + +@Composable +private fun showCameraErrorText() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.warning_icon), + contentDescription = "Warning icon", + ) + Spacer(modifier = Modifier.width(8.dp)) // Space between icon and text + Text( + text = stringResource(R.string.instruction_6), + style = TextStyle( + fontSize = 14.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_regular)), + color = Variables.blackText, + ) + ) + } + } +} + +@Composable +fun DrawOCRResult( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float +) { + Canvas( // Layer 3 + modifier = Modifier + .fillMaxSize() + ) { + uiState.ocrResults.forEach { ocrResultData -> + if (ocrResultData.text.isNotEmpty()) { + + val bBoxTop = ocrResultData.boundingBox.top.toFloat() + val bBoxLeft = ocrResultData.boundingBox.left.toFloat() + val bBoxBottom = ocrResultData.boundingBox.bottom.toFloat() + val bBoxRight = ocrResultData.boundingBox.right.toFloat() + + val scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX + val scaledBBoxTopInPx = (scaler * bBoxTop) + gapY + val scaledBBoxRightInPx = (scaler * bBoxRight) + gapX + val scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + + // Define the size and position of the rectangle + val rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + val rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + + // Draw the filled rectangle + drawRect( + color = Color(0xBF000000), + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight) + ) + + // Draw the border over the filled rectangle + drawRect( + color = Color(0xFFFF7B00), + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (1f * displayMetricsDensity)) + ) + + // Prepare to draw the text + val paint = android.graphics.Paint().apply { + color = android.graphics.Color.WHITE + textAlign = android.graphics.Paint.Align.CENTER + } + + // Calculate the maximum text size that fits in the rectangle + val padding = 0.5f * displayMetricsDensity // Padding from the border + var textSize = 2f + + // Incrementally increase text size until it just fits + do { + paint.textSize = textSize + val textWidth = paint.measureText(ocrResultData.text) + val textHeight = paint.descent() - paint.ascent() + if (textWidth + padding * 2 <= rectangleWidth && textHeight + padding * 2 <= rectangleHeight) { + textSize += 1f + } else { + break + } + } while (true) + + // Adjust the text size to be slightly smaller + paint.textSize = textSize - 1f + + // Calculate the position to draw the text + val textOffsetX = topLeftOffset.x + rectangleWidth / 2 + val textOffsetY = + topLeftOffset.y + rectangleHeight / 2 - (paint.ascent() + paint.descent()) / 2 + + // Draw the text using nativeCanvas + drawContext.canvas.nativeCanvas.drawText( + ocrResultData.text, + textOffsetX, + textOffsetY, + paint + ) + } + } + } +} + +@Composable +fun DrawOCRResultWithTextSizeScaling( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float, + displayTotalHeightInPx: Int, + displayTotalWidthInPx: Int +) { + Canvas( // Layer 3 + modifier = Modifier + .fillMaxSize() + ) { + uiState.ocrResults.forEach { ocrResultData -> + if (ocrResultData.text.isNotEmpty()) { + + val bBoxTop = ocrResultData.boundingBox.top.toFloat() + val bBoxLeft = ocrResultData.boundingBox.left.toFloat() + val bBoxBottom = ocrResultData.boundingBox.bottom.toFloat() + val bBoxRight = ocrResultData.boundingBox.right.toFloat() + + var scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX + var scaledBBoxTopInPx = (scaler * bBoxTop) + gapY + var scaledBBoxRightInPx = (scaler * bBoxRight) + gapX + var scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + + // Define the size and position of the rectangle + var rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + var rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + + // This is preventing the Text to show too small on the drawing + if (rectangleHeight <= 20f || rectangleWidth <= 20f) { + + // Firstly, try increase the BBox Height by 40Px + scaledBBoxTopInPx -= 20f + + // Make sure, the scaling fit within the Screen at Top. + if (scaledBBoxTopInPx < 0) { + scaledBBoxTopInPx = 0f + } + + scaledBBoxBottomInPx += 20f + // Make sure, the scaling fit within the Screen at Bottom. + if (scaledBBoxBottomInPx > displayTotalHeightInPx.toFloat()) { + scaledBBoxBottomInPx = displayTotalHeightInPx.toFloat() + } + + // recalculate the height + rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + + // Secondly, try increase the BBox Width by 40Px + scaledBBoxLeftInPx -= 20f + + // Make sure, the scaling fit within the Screen at Left. + if (scaledBBoxLeftInPx < 0) { + scaledBBoxLeftInPx = 0f + } + + scaledBBoxRightInPx += 20f + // Make sure, the scaling fit within the Screen at Right. + if (scaledBBoxRightInPx > displayTotalWidthInPx.toFloat()) { + scaledBBoxRightInPx = displayTotalWidthInPx.toFloat() + } + + // recalculate the Width + rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + } + + val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + + // Draw the filled rectangle + drawRect( + color = Color(0xBF000000), + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight) + ) + + // Draw the border over the filled rectangle + drawRect( + color = Color(0xFFFF7B00), + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (1f * displayMetricsDensity)) + ) + + // Prepare to draw the text + val paint = android.graphics.Paint().apply { + color = android.graphics.Color.WHITE + textAlign = android.graphics.Paint.Align.CENTER + } + + // Calculate the maximum text size that fits in the rectangle + val padding = 0.5f * displayMetricsDensity // Padding from the border + var textSize = 2f + + // Incrementally increase text size until it just fits + do { + paint.textSize = textSize + val textWidth = paint.measureText(ocrResultData.text) + val textHeight = paint.descent() - paint.ascent() + if (textWidth + padding * 2 <= rectangleWidth && textHeight + padding * 2 <= rectangleHeight) { + textSize += 1f + } else { + break + } + } while (true) + + // Adjust the text size to be slightly smaller + paint.textSize = textSize - 1f + + // Calculate the position to draw the text + val textOffsetX = topLeftOffset.x + rectangleWidth / 2 + val textOffsetY = + topLeftOffset.y + rectangleHeight / 2 - (paint.ascent() + paint.descent()) / 2 + + // Draw the text using nativeCanvas + drawContext.canvas.nativeCanvas.drawText( + ocrResultData.text, + textOffsetX, + textOffsetY, + paint + ) + } + } + } +} + +@Composable +fun DrawOCRResultWithTextSizeScalingAndCheckMark( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float, + checkIconDrawable: Drawable?, + displayTotalHeightInPx: Int, + displayTotalWidthInPx: Int +) { + Canvas( // Layer 3 + modifier = Modifier + .fillMaxSize() + ) { + uiState.ocrResults.forEach { ocrResultData -> + if (ocrResultData.text.isNotEmpty()) { + + val bBoxTop = ocrResultData.boundingBox.top.toFloat() + val bBoxLeft = ocrResultData.boundingBox.left.toFloat() + val bBoxBottom = ocrResultData.boundingBox.bottom.toFloat() + val bBoxRight = ocrResultData.boundingBox.right.toFloat() + + var scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX + var scaledBBoxTopInPx = (scaler * bBoxTop) + gapY + var scaledBBoxRightInPx = (scaler * bBoxRight) + gapX + var scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + + // Define the size and position of the rectangle + var rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + var rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + + // This is preventing the Text to show too small on the drawing + if (rectangleHeight <= 20f || rectangleWidth <= 20f) { + + // Firstly, try increase the BBox Height by 40Px + scaledBBoxTopInPx -= 20f + + // Make sure, the scaling fit within the Screen at Top. + if (scaledBBoxTopInPx < 0) { + scaledBBoxTopInPx = 0f + } + + scaledBBoxBottomInPx += 20f + // Make sure, the scaling fit within the Screen at Bottom. + if (scaledBBoxBottomInPx > displayTotalHeightInPx.toFloat()) { + scaledBBoxBottomInPx = displayTotalHeightInPx.toFloat() + } + + // recalculate the height + rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + + // Secondly, try increase the BBox Width by 40Px + scaledBBoxLeftInPx -= 20f + + // Make sure, the scaling fit within the Screen at Left. + if (scaledBBoxLeftInPx < 0) { + scaledBBoxLeftInPx = 0f + } + + scaledBBoxRightInPx += 20f + // Make sure, the scaling fit within the Screen at Right. + if (scaledBBoxRightInPx > displayTotalWidthInPx.toFloat()) { + scaledBBoxRightInPx = displayTotalWidthInPx.toFloat() + } + + // recalculate the Width + rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + } + + val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + + // Draw the filled rectangle + drawRect( + color = Color(0xBF000000), + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight) + ) + + // Draw the border over the filled rectangle + drawRect( + color = Color(0xFFFF7B00), + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (1f * displayMetricsDensity)) + ) + + // Prepare to draw the text + val paint = android.graphics.Paint().apply { + color = android.graphics.Color.WHITE + textAlign = android.graphics.Paint.Align.CENTER + } + + // Calculate the maximum text size that fits in the rectangle + val padding = 0.5f * displayMetricsDensity // Padding from the border + var textSize = 2f + + // Incrementally increase text size until it just fits + do { + paint.textSize = textSize + val textWidth = paint.measureText(ocrResultData.text) + val textHeight = paint.descent() - paint.ascent() + if (textWidth + padding * 2 <= rectangleWidth && textHeight + padding * 2 <= rectangleHeight) { + textSize += 1f + } else { + break + } + } while (true) + + // Adjust the text size to be slightly smaller + paint.textSize = textSize - 1f + + // Calculate the position to draw the text + val textOffsetX = topLeftOffset.x + rectangleWidth / 2 + val textOffsetY = + topLeftOffset.y + rectangleHeight / 2 - (paint.ascent() + paint.descent()) / 2 + + // Draw the text using nativeCanvas + drawContext.canvas.nativeCanvas.drawText( + ocrResultData.text, + textOffsetX, + textOffsetY, + paint + ) + + checkIconDrawable?.let { icon -> + icon.setBounds( + (scaledBBoxLeftInPx + (rectangleWidth / 2) - 30F).toInt(), + (scaledBBoxTopInPx - 30F - 35f).toInt(), + (scaledBBoxLeftInPx + (rectangleWidth / 2) + 30F).toInt(), + (scaledBBoxTopInPx + 30F - 35f).toInt() + ) + icon.draw(drawContext.canvas.nativeCanvas) + + } + } + } + } +} + +@Composable +fun DrawBarcodeResult( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float +) { + Canvas( + modifier = Modifier + .fillMaxSize() + ) { + uiState.barcodeResults.forEach { barcodeData -> + barcodeData?.let { + + val bBoxTop = barcodeData.boundingBox.top.toFloat() + val bBoxLeft = barcodeData.boundingBox.left.toFloat() + val bBoxBottom = barcodeData.boundingBox.bottom.toFloat() + val bBoxRight = barcodeData.boundingBox.right.toFloat() + + val scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX + val scaledBBoxTopInPx = (scaler * bBoxTop) + gapY + val scaledBBoxRightInPx = (scaler * bBoxRight) + gapX + val scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + + // Define the size and position of the rectangle + val rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + val rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + + drawRect( + color = Color.Green, + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (1f * displayMetricsDensity)) + ) + } + } + } + + // Draw Decoded Text if found + uiState.barcodeResults.forEach { barcodeData -> + barcodeData?.let { + val bBoxLeft = barcodeData.boundingBox.left.toFloat() + val bBoxBottom = barcodeData.boundingBox.bottom.toFloat() + + val scaledBBoxLeftInDp = (((scaler * bBoxLeft) + gapX) / displayMetricsDensity).dp + val scaledBBoxBottomInDp = (((scaler * bBoxBottom) + gapY) / displayMetricsDensity).dp + + if (barcodeData.text != null && barcodeData.text != "") { + Text( + text = barcodeData.text, + fontSize = 10.sp, + color = Color.White, + style = TextStyle( + platformStyle = PlatformTextStyle( + includeFontPadding = false + ) + ), + modifier = Modifier + .offset(x = scaledBBoxLeftInDp, y = scaledBBoxBottomInDp + 2.dp) + .background(Color(0xBF000000)) + .padding(2.dp) + ) + } + } + } +} + +@Composable +private fun DrawModuleRecognitionResult( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float +) { + Canvas( + modifier = Modifier + .fillMaxSize() + ) { + uiState.moduleResults.shelves.forEach { + it.let { shelf -> + + val bBoxTop = shelf.boundingBox.top.toFloat() + val bBoxLeft = shelf.boundingBox.left.toFloat() + val bBoxBottom = shelf.boundingBox.bottom.toFloat() + val bBoxRight = shelf.boundingBox.right.toFloat() + + val scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX + val scaledBBoxTopInPx = (scaler * bBoxTop) + gapY + val scaledBBoxRightInPx = (scaler * bBoxRight) + gapX + val scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + + val rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + val rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + + drawRect( + color = Color.Red, + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (1.5f * displayMetricsDensity)) + ) + } + } + uiState.moduleResults.labelEntity.forEach { + it.let { labelEntity -> + + val bBoxTop = labelEntity.boundingBox.top.toFloat() + val bBoxLeft = labelEntity.boundingBox.left.toFloat() + val bBoxBottom = labelEntity.boundingBox.bottom.toFloat() + val bBoxRight = labelEntity.boundingBox.right.toFloat() + + val scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX + val scaledBBoxTopInPx = (scaler * bBoxTop) + gapY + val scaledBBoxRightInPx = (scaler * bBoxRight) + gapX + val scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + + val rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + val rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + + drawRect( + color = Color.Blue, + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (1.5f * displayMetricsDensity)) + ) + + val barcodes = labelEntity.barcodes + Log.d(TAG, "Barcodes size: " + barcodes.size) + if (!barcodes.isEmpty()) { + barcodes.forEach { barcode -> + val barcodeRect = barcode.boundingBox + + val bBarcodeBoxTop = barcodeRect.top.toFloat() + val bBarcodeBoxLeft = barcodeRect.left.toFloat() + val bBarcodeBoxBottom = barcodeRect.bottom.toFloat() + val bBarcodeBoxRight = barcodeRect.right.toFloat() + + val scaledBarcodeBBoxLeftInPx = (scaler * bBarcodeBoxLeft) + gapX + val scaledBarcodeBBoxTopInPx = (scaler * bBarcodeBoxTop) + gapY + val scaledBarcodeBBoxRightInPx = (scaler * bBarcodeBoxRight) + gapX + val scaledBarcodeBBoxBottomInPx = (scaler * bBarcodeBoxBottom) + gapY + + val barcodeRectangleHeight = scaledBarcodeBBoxBottomInPx - scaledBarcodeBBoxTopInPx + Log.d(TAG, "Detected entity - Value: " + barcode.value) + Log.d(TAG, "Detected entity - Symbology: " + barcode.symbology) + + val paint = android.graphics.Paint().apply { + color = android.graphics.Color.WHITE + textSize = 30f + } + + val barcodeTextOffset = Offset( + scaledBarcodeBBoxLeftInPx, + scaledBarcodeBBoxTopInPx + (barcodeRectangleHeight) / 2 + ) + drawContext.canvas.nativeCanvas.drawText( + barcode.value, + barcodeTextOffset.x, + barcodeTextOffset.y, + paint + ) + } + } + } + } + uiState.moduleResults.productEntity.forEach { + it.let { productEntity -> + + val bBoxTop = productEntity.boundingBox.top.toFloat() + val bBoxLeft = productEntity.boundingBox.left.toFloat() + val bBoxBottom = productEntity.boundingBox.bottom.toFloat() + val bBoxRight = productEntity.boundingBox.right.toFloat() + + val scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX + val scaledBBoxTopInPx = (scaler * bBoxTop) + gapY + val scaledBBoxRightInPx = (scaler * bBoxRight) + gapX + val scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + + val rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + val rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + + val topSku = productEntity.topKSKUs?.firstOrNull()?.let { + if (it.accuracy > uiState.retailShelfSettings.similarityThreshold / 100) it.productSKU else "" + } ?: "" + if(topSku.isEmpty()) { + drawRect( + color = Color.Green, + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (1.5f * displayMetricsDensity)) + ) + } else { + drawRect( + color = Color(0xAA004830).copy(alpha = 0.5F), + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight) + ) + + val paint = android.graphics.Paint().apply { + color = android.graphics.Color.WHITE + textSize = 30f + } + + val textOffset = Offset( + scaledBBoxLeftInPx, + scaledBBoxTopInPx + (rectangleHeight) / 2 + ) + drawContext.canvas.nativeCanvas.drawText( + topSku, + textOffset.x, + textOffset.y, + paint + ) + } + } + } + + } +} + +@Composable +private fun DrawRetailShelfResult( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float +) { + Canvas( + modifier = Modifier + .fillMaxSize() + ) { + uiState.bboxes.forEach { bBox -> + bBox?.let { + + val bBoxTop = bBox.ymin + val bBoxLeft = bBox.xmin + val bBoxBottom = bBox.ymax + val bBoxRight = bBox.xmax + + val scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX + val scaledBBoxTopInPx = (scaler * bBoxTop) + gapY + val scaledBBoxRightInPx = (scaler * bBoxRight) + gapX + val scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + + val rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + val rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + + val boxColor = when (bBox.cls) { + 1 -> Color.Green // Products + 2 -> Color.Blue // Shelf Labels + 3 -> Color.Blue // Peg Labels + 4 -> Color.Red // Shelf Row + else -> { + Color.Magenta // unknown + } + } + drawRect( + color = boxColor, + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (1.5f * displayMetricsDensity)) + ) + } + } + } +} + +@Composable +private fun DrawProductRecognitionResult( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float +) { + Canvas( + modifier = Modifier + .fillMaxSize() + ) { + uiState.productResults.forEach { productResult -> + if (productResult.text.isNotEmpty()) { + + val bBoxTop = productResult.bBox.ymin + val bBoxLeft = productResult.bBox.xmin + val bBoxBottom = productResult.bBox.ymax + val bBoxRight = productResult.bBox.xmax + + val scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX + val scaledBBoxTopInPx = (scaler * bBoxTop) + gapY + val scaledBBoxRightInPx = (scaler * bBoxRight) + gapX + val scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + + val rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + val rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + + // Draw the filled rectangle + drawRect( + color = Color(0xAA004830).copy(alpha = 0.5F), + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight) + ) + + val paint = android.graphics.Paint().apply { + color = android.graphics.Color.WHITE + textSize = 30f + } + + val textOffset = Offset( + scaledBBoxLeftInPx, + scaledBBoxTopInPx + (rectangleHeight) / 2 + ) + drawContext.canvas.nativeCanvas.drawText( + productResult.text, + textOffset.x, + textOffset.y, + paint + ) + } + } + } +} + +@Composable +fun showBottomBar( + navController: NavController, + viewModel: AIDataCaptureDemoViewModel, + activityInnerPadding: PaddingValues +) { + val uiState = viewModel.uiState.collectAsState().value + var torchEnabled = remember { mutableStateOf(false) } + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(bottom = activityInnerPadding.calculateBottomPadding()) + .background(Color.Black.copy(alpha = 0.4f)), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .padding(start = 12.dp, end = 12.dp, top = 10.dp, bottom = 10.dp) + ) { + Box( + modifier = Modifier + .size(50.dp) + .align(Alignment.CenterVertically) + .background( + color = if (torchEnabled.value) { + Color.White.copy(alpha = 0.4f) + } else { + Color.Black.copy(alpha = 0.4f) + }, + shape = RoundedCornerShape(percent = 50) + ) + .clickable { + torchEnabled.value = !torchEnabled.value + viewModel.enableTorch(torchEnabled.value) + }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = ImageVector.Companion.vectorResource(R.drawable.flashlight_icon), + contentDescription = "Torch", + modifier = Modifier + .size(20.dp), + tint = if (torchEnabled.value) Variables.mainDefault else Variables.stateDefaultEnabled + ) + } + if ((uiState.usecaseSelected == UsecaseState.Product.value) || + (uiState.usecaseSelected == UsecaseState.BarcodeMap.value) || + ((uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value) && (uiState.isCaptureOrLiveEnabled == 0))){ + var isClickable = remember { mutableStateOf(true) } + Icon( + imageVector = ImageVector.Companion.vectorResource(R.drawable.shutter_button), + contentDescription = "Capture Image", + modifier = Modifier + .size(70.dp) + .padding(4.dp) + .clickable(enabled = isClickable.value) { + isClickable.value = false + viewModel.viewModelScope.launch { + // Stop analysing the Preview Frames + viewModel.stopPreviewAnalysis() + + // Grab High Res Bitmap + val highResBitmap = viewModel.takePicture() + + // set the High Res Bitmap to ViewModel + viewModel.updateCaptureBitmap(bitmap = highResBitmap) + + // Send the High Res Image for Processing + viewModel.executeHighRes(highResBitmap = highResBitmap) + + if (uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value) { + viewModel.updateOcrBarcodeCaptureSessionCount(uiState.ocrBarcodeCaptureSessionCount + 1) + navController.navigate(route = Screen.OCRBarcodeCapture.route) + } else if (uiState.usecaseSelected == UsecaseState.BarcodeMap.value) { + navController.navigate(route = Screen.BarcodeMapResults.route) + } else { + navController.navigate(route = Screen.ProductsCapture.route) + } + } + }, + tint = Variables.stateDefaultEnabled + ) + } + Box( + modifier = Modifier + .size(50.dp) + .align(Alignment.CenterVertically) + .background( + Color.Black.copy(alpha = 0.4f), shape = RoundedCornerShape(percent = 50) + ) + .clickable { + var zoomRatio: Float = uiState.zoomLevel * 2.0f + if (zoomRatio > 4.0f) { + zoomRatio = 1.0F + } + viewModel.setZoom(zoomRatio) + }, + contentAlignment = Alignment.Center + ) { + val roundedValue = ((uiState.zoomLevel * 10).toInt()).toFloat() / 10 + Text( + text = roundedValue.toString() + "x", + style = TextStyle( + fontSize = 12.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_regular)), + fontWeight = FontWeight(400), + color = Variables.mainInverse + ) + ) + } + } + } + } +} + +@Composable +fun HandleTopInfo( + icon: Int, + info: String, + showInfo: MutableState +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Transparent) + .padding(top = 16.dp, start = 21.dp, end = 21.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .border( + width = 0.87838.dp, + color = Color(0xFFF8D249), + shape = RoundedCornerShape(size = 360.13556.dp) + ) + .background( + color = Color(0xD9151519), + shape = RoundedCornerShape(size = 360.13556.dp) + ) + ) { + Row( + modifier = Modifier.padding( + top = Variables.spacingLarge, + bottom = Variables.spacingMedium + ), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.width(Variables.spacingLarge)) + Image( + painter = painterResource(id = icon), + contentDescription = "shutter", + ) + Spacer(modifier = Modifier.width(Variables.spacingLarge)) + Text( + text = info, + modifier = Modifier + .padding(end = 36.dp) + .weight(1f), + style = TextStyle( + fontSize = 14.sp, + lineHeight = 21.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainInverse, + ) + ) + Image( + painter = painterResource(id = R.drawable.icon_close), + contentDescription = "close", + modifier = Modifier + .clickable { + showInfo.value = false + } + .background( + Color.Black, + shape = RoundedCornerShape(size = 360.13556.dp) + ) + ) + Spacer(modifier = Modifier.width(Variables.spacingLarge)) + } + } + } +} + +@Composable +fun showInformationBox( + info: String, + topPadding: Dp +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Transparent) + .padding(top = topPadding, start = 21.dp, end = 21.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .border( + width = 0.87838.dp, + color = Color(0xFFF8D249), + shape = RoundedCornerShape(size = 360.13556.dp) + ) + .background( + Color.Black.copy(alpha = 0.4f), + shape = RoundedCornerShape(size = 360.13556.dp) + ) + ) { + Row( + modifier = Modifier.padding( + top = Variables.spacingLarge, + bottom = Variables.spacingMedium + ), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.width(Variables.spacingLarge)) + Icon( + imageVector = Icons.Filled.Search, + contentDescription = "Search Icon", + tint = Variables.mainInverse + ) + Spacer(modifier = Modifier.width(Variables.spacingLarge)) + Text( + text = info, + modifier = Modifier + .padding(end = 36.dp) + .weight(1f), + style = TextStyle( + fontSize = 14.sp, + lineHeight = 21.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainInverse, + ) + ) + Spacer(modifier = Modifier.width(Variables.spacingLarge)) + } + } + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CommonUIElements.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CommonUIElements.kt new file mode 100644 index 0000000..4f10e19 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CommonUIElements.kt @@ -0,0 +1,721 @@ +package com.zebra.aidatacapturedemo.ui.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.TextSelectionColors +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +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.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.ui.view.Variables.mainDisabled +import com.zebra.aidatacapturedemo.ui.view.Variables.mainInverse +import com.zebra.aidatacapturedemo.ui.view.Variables.mainPrimary + +/** + * CommonUIElements.kt + * + * This file contains reusable composable functions for common UI elements such as radio buttons, + * switches, text inputs, and buttons. These components are designed to be flexible and customizable, + * allowing them to be used across different screens in the application. + * + * Each composable function is accompanied by a corresponding data class that encapsulates the + * necessary information and callbacks for that UI element. + * This approach promotes separation of concerns and makes it easier to manage state and + * interactions within the UI. + * + * The components are styled according to the application's design guidelines, utilizing colors, + * typography, and spacing defined in the Variables object. + */ +data class RadioButtonData( + val title: String, + val description: Int?, + val index: Int, + val onItemSelected: (itemId: Int) -> Unit // A callback with a String parameter +) + +@Composable +fun ListOfRadioButtonOptions(currentSelection: Int, radioOptions: List) { + val (selectedOption, onOptionSelected) = remember { mutableStateOf(radioOptions[currentSelection]) } + + Column(Modifier.selectableGroup()) { // Modifier.selectableGroup() is crucial for accessibility + radioOptions.forEach { item -> + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background( + color = Variables.surfaceDefault, + shape = RoundedCornerShape(size = 3.6.dp) + ) + .padding(start = 14.4.dp, top = 8.dp, end = 14.4.dp, bottom = 8.dp) + .selectable( + selected = (item == selectedOption), + onClick = { + onOptionSelected(item) + item.onItemSelected(item.index) + } + ), + horizontalArrangement = Arrangement.spacedBy( + 10.799999237060547.dp, + Alignment.Start + ), + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = item.title, + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainDefault, + ) + ) + item.description?.let { + Text( + text = stringResource(it), + style = TextStyle( + fontSize = 12.sp, + lineHeight = 16.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainDefault, + ) + ) + } + + } + Spacer(modifier = Modifier.weight(1f)) + RadioButton( + modifier = Modifier + .background(color = Variables.surfaceDefault), + colors = RadioButtonDefaults.colors( + selectedColor = Variables.mainPrimary, + unselectedColor = Variables.mainDefault, + disabledSelectedColor = Variables.mainDisabled, + disabledUnselectedColor = Variables.mainDisabled + ), + selected = (item == selectedOption), + onClick = { + onOptionSelected(item) + item.onItemSelected(item.index) + } + ) + } + } + } +} + +data class SwitchOptionData( + val titleId: Int, + val onItemSelected: (title: String, selected: Boolean) -> Unit // A callback with a String parameter +) + +@Composable +fun SwitchOptionForModelSelectionScreen(currentValue: Boolean, switchOption: SwitchOptionData) { + var title = stringResource(switchOption.titleId) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + verticalAlignment = Alignment.Top, + modifier = Modifier + .wrapContentHeight() + .background(color = Color(0xFFFFFFFF), shape = RoundedCornerShape(size = 3.6.dp)) + .padding(start = 14.4.dp, top = 8.dp, end = 14.4.dp, bottom = 8.dp) + ) { + Text( + text = title, + modifier = Modifier + .width(256.dp) + .height(22.dp), + style = TextStyle( + fontSize = 14.4.sp, + lineHeight = 21.6.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_regular)), + fontWeight = FontWeight(500), + color = Color(0xFF1D1E23), + ) + ) + Spacer(modifier = Modifier.weight(1f)) + Switch( + checked = currentValue, + onCheckedChange = { + switchOption.onItemSelected(title, it) + }, + colors = SwitchDefaults.colors( + checkedThumbColor = mainInverse, + checkedTrackColor = mainPrimary, + uncheckedThumbColor = mainInverse, + uncheckedTrackColor = mainDisabled, + uncheckedBorderColor = Color.Transparent + ), + thumbContent = { + Box( + modifier = Modifier + .size(18.dp) + .background( + color = Variables.surfaceDefault, + shape = CircleShape + ) + ) + }, + modifier = Modifier + .width(43.2.dp) + .height(21.6.dp) + ) + } +} + +@Composable +fun SwitchOption(currentValue: Boolean, switchOption: SwitchOptionData) { + var isChecked by remember { mutableStateOf(currentValue) } + var title = stringResource(switchOption.titleId) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + verticalAlignment = Alignment.Top, + modifier = Modifier + .wrapContentHeight() + .background(color = Color(0xFFFFFFFF), shape = RoundedCornerShape(size = 3.6.dp)) + .padding(start = 14.4.dp, top = 8.dp, end = 14.4.dp, bottom = 8.dp) + ) { + Text( + text = title, + modifier = Modifier + .width(256.dp) + .height(22.dp), + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.weight(1f)) + Switch( + checked = isChecked, + onCheckedChange = { + isChecked = it + switchOption.onItemSelected(title, isChecked) + }, + colors = SwitchDefaults.colors( + checkedThumbColor = mainInverse, + checkedTrackColor = mainPrimary, + uncheckedThumbColor = mainInverse, + uncheckedTrackColor = mainDisabled, + uncheckedBorderColor = Color.Transparent + ), + thumbContent = { + Box( + modifier = Modifier + .size(18.dp) + .background( + color = Variables.surfaceDefault, + shape = CircleShape + ) + ) + }, + modifier = Modifier + .width(43.2.dp) + .height(21.6.dp) + ) + } +} + +@Composable +fun SwitchOptionWithTextDescription(currentValue: Boolean, switchOption: SwitchOptionData, description : String) { + var isChecked by remember { mutableStateOf(currentValue) } + var title = stringResource(switchOption.titleId) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + verticalAlignment = Alignment.Top, + modifier = Modifier + .wrapContentHeight() + .background(color = Color(0xFFFFFFFF), shape = RoundedCornerShape(size = 3.6.dp)) + .padding(start = 14.4.dp, top = 8.dp, end = 14.4.dp, bottom = 8.dp) + ) { + Column { + Text( + text = title, + modifier = Modifier + .width(256.dp), + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainDefault, + ) + ) + Text( + text = description, + modifier = Modifier + .width(256.dp), + style = TextStyle( + fontSize = 12.sp, + lineHeight = 16.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainDefault, + ) + ) + } + Spacer(modifier = Modifier.weight(1f)) + Switch( + checked = isChecked, + onCheckedChange = { + isChecked = it + switchOption.onItemSelected(title, isChecked) + }, + colors = SwitchDefaults.colors( + checkedThumbColor = mainInverse, + checkedTrackColor = mainPrimary, + uncheckedThumbColor = mainInverse, + uncheckedTrackColor = mainDisabled, + uncheckedBorderColor = Color.Transparent + ), + thumbContent = { + Box( + modifier = Modifier + .size(18.dp) + .background( + color = Variables.surfaceDefault, + shape = CircleShape + ) + ) + }, + modifier = Modifier + .width(43.2.dp) + .height(21.6.dp) + ) + } +} + +data class TextInputData( + val titleId: Int, + val currentValue: String, + val placeholder: String = "", + val onItemSelected: (title: String, newValue: String) -> Unit +) + +@Composable +fun TextInputOption(textInputOption: TextInputData, enabled: Boolean = true) { + var text by remember { mutableStateOf(textInputOption.currentValue) } + var title = stringResource(textInputOption.titleId) + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OutlinedTextField( + value = text, + enabled = enabled, + onValueChange = { + text = it + textInputOption.onItemSelected(title, text) + }, + label = { Text(title) }, + placeholder = { Text(textInputOption.placeholder) }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done + ), + modifier = Modifier + .background(color = Color(0xFFFFFFFF), shape = RoundedCornerShape(size = 3.6.dp)) + .fillMaxWidth() + .padding(start = 14.4.dp, top = 8.dp, end = 14.4.dp, bottom = 8.dp), + colors = TextFieldDefaults.colors( + focusedContainerColor = Variables.surfaceDefault, + unfocusedContainerColor = Variables.surfaceDefault, + cursorColor = Variables.mainPrimary, + focusedIndicatorColor = Variables.mainPrimary, + unfocusedIndicatorColor = Variables.mainPrimary, + unfocusedLabelColor = Variables.mainSubtle, + focusedLabelColor = Variables.mainSubtle, + selectionColors = TextSelectionColors( + handleColor = mainPrimary, + backgroundColor = mainPrimary + ), + disabledContainerColor = Variables.surfaceDefault, + disabledLabelColor = Variables.mainDisabled + ) + ) + } +} + +data class ButtonData( + val titleId: Int, + val color: Color, + val alpha: Float = 1.0F, + val enabled: Boolean, + val onButtonClick: () -> Unit +) + +@Composable +fun ButtonOption(buttonData: ButtonData) { + Row( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 12.dp), + horizontalArrangement = Arrangement.Center + ) { + Row( + horizontalArrangement = Arrangement.spacedBy( + Variables.spacingSmall, + Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .alpha(buttonData.alpha) + .background(color = buttonData.color, shape = RoundedCornerShape(size = 4.dp)) + .padding( + start = Variables.spacingLarge, + top = Variables.spacingMedium, + end = Variables.spacingLarge, + bottom = Variables.spacingMedium + ) + .clickable(enabled = buttonData.enabled) { + buttonData.onButtonClick() + } + ) { + Text( + text = stringResource(buttonData.titleId), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_regular)), + fontWeight = FontWeight(500), + color = Variables.stateDefaultEnabled, + textAlign = TextAlign.Center, + ) + ) + } + } +} +@Composable +fun ButtonWithIconOption(buttonData: ButtonData, drawableRes: Int) { + Row( + modifier = Modifier.padding(end = 8.dp), + horizontalArrangement = Arrangement.End + ) { + Row( + horizontalArrangement = Arrangement.spacedBy( + Variables.spacingSmall, + Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .alpha(buttonData.alpha) + .background(color = buttonData.color, shape = RoundedCornerShape(size = 4.dp)) + .padding( + start = Variables.spacingLarge, + top = Variables.spacingMedium, + end = Variables.spacingLarge, + bottom = Variables.spacingMedium + ) + .clickable(enabled = buttonData.enabled) { + buttonData.onButtonClick() + } + ) { + Icon( + painter = painterResource(id = drawableRes), + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = Color.White + ) + Text( + text = stringResource(buttonData.titleId), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_regular)), + fontWeight = FontWeight(500), + color = Variables.stateDefaultEnabled, + textAlign = TextAlign.Center, + ) + ) + } + } +} +data class BorderlessButtonData( + val titleId: Int, + val onButtonClick: () -> Unit +) + +@Composable +fun BorderlessButton(buttonData: BorderlessButtonData) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .background( + color = Variables.stateDefaultEnabled, + shape = RoundedCornerShape(size = 4.dp) + ) + .padding(top = Variables.spacingSmall, bottom = Variables.spacingSmall) + .clickable { + buttonData.onButtonClick() + } + ) { + Text( + text = stringResource(buttonData.titleId), + softWrap = true, + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Variables.borderPrimaryMain, + ) + ) + } +} + +@Composable +fun TextviewNormal(info: String) { + Text( + text = info, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + style = TextStyle( + fontSize = 14.4.sp, + lineHeight = 21.6.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_regular)), + fontWeight = FontWeight(500), + color = Color(0xFF1D1E23), + ) + ) +} + +@Composable +fun TextviewBold(info: String) { + Text( + text = info, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + style = TextStyle( + fontSize = 14.4.sp, + lineHeight = 21.6.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Color(0xFF1D1E23), + ) + ) +} + +@Composable +fun RoundIconButton( + drawableRes: Int, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + IconButton( + onClick = onClick, + modifier = modifier + .background(color = Variables.backgroundDark, shape = CircleShape) + .width(40.dp) + .height(40.dp) + ) { + Icon( + painter = painterResource(id = drawableRes), + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = Color.White + ) + } +} + +@Composable +fun RoundCloseButton(onClick: () -> Unit, contentDescription: String = "Close") { + IconButton( + onClick = onClick, + modifier = Modifier + .background(color = Variables.backgroundDark, shape = CircleShape) + .width(40.dp) + .height(40.dp) + ) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = contentDescription, + tint = Color.White + ) + } +} + + +@Composable +fun RoundedIconButton(icon: Int, onClick: () -> Unit, contentDescription: String = "Next") { + IconButton( + onClick = onClick, + modifier = Modifier + .border(width = 1.dp, color = Variables.borderSubtle, shape = RoundedCornerShape(size = 4.dp)) + .width(45.21143.dp) + .height(44.dp) + .background(color = Variables.stateDefaultEnabled, shape = RoundedCornerShape(size = 4.dp)) + .padding(start = Variables.spacingSmall, top = Variables.spacingSmall, end = Variables.spacingSmall, bottom = Variables.spacingSmall) + ) { + Icon( + painter = painterResource(id = icon), + contentDescription = contentDescription, + tint = Color.Black + ) + } +} + + +@Composable +fun EmptyComposable() { + // Intentionally left blank +} + +@Composable +fun SingleValueInputSlider( + value: Float, + onValueChange: (Float) -> Unit, + valueRange: ClosedFloatingPointRange = 0f..100f, + steps: Int = 0, + label: String = "Value" +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("$label: ${value.toInt()}") + Slider( + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + steps = steps, + modifier = Modifier.fillMaxWidth(), + colors = SliderDefaults.colors( + thumbColor = mainPrimary, + activeTrackColor = mainPrimary, + inactiveTrackColor = mainPrimary.copy(alpha = 0.24f) + ) + ) + } +} + +@Composable +fun SmallScreenButtonOption(buttonData: ButtonData) { + Row( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = Variables.spacingSmall, bottom = Variables.spacingSmall), + horizontalArrangement = Arrangement.Center + ) { + Row( + horizontalArrangement = Arrangement.spacedBy( + Variables.spacingSmall, + Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .alpha(buttonData.alpha) + .background(color = buttonData.color, shape = RoundedCornerShape(size = 4.dp)) + .padding( + start = Variables.spacingLarge, + top = Variables.spacingMedium, + end = Variables.spacingLarge, + bottom = Variables.spacingMedium + ) + .clickable(enabled = buttonData.enabled) { + buttonData.onButtonClick() + } + ) { + Text( + text = stringResource(buttonData.titleId), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + style = TextStyle( + fontSize = 12.sp, + lineHeight = 16.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_regular)), + fontWeight = FontWeight(500), + color = Variables.stateDefaultEnabled, + textAlign = TextAlign.Center, + ) + ) + } + } +} + +@Composable +fun SmallScreenTextviewNormal(info: String) { + Text( + text = info, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + style = TextStyle( + fontSize = 14.4.sp, + lineHeight = 21.6.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_regular)), + fontWeight = FontWeight(500), + color = Color(0xFF1D1E23), + ) + ) +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomBulletSpan.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomBulletSpan.kt new file mode 100644 index 0000000..b66185a --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomBulletSpan.kt @@ -0,0 +1,79 @@ +package com.zebra.aidatacapturedemo.ui.view + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path +import android.graphics.Path.Direction +import android.text.Layout +import android.text.Spanned +import android.text.style.LeadingMarginSpan +import android.util.TypedValue + +/** + * CustomBulletSpan is a custom implementation of LeadingMarginSpan to create bullet points with + * customizable radius, gap width, and color. It draws a circle as the bullet point and allows + * for better control over the appearance of the bullets in a list. + */ +class CustomBulletSpan( + val bulletRadius: Int = STANDARD_BULLET_RADIUS, + val gapWidth: Int = STANDARD_GAP_WIDTH, + val color: Int = STANDARD_COLOR +) : LeadingMarginSpan { + + companion object { + // Bullet is slightly bigger to avoid aliasing artifacts on mdpi devices. + private const val STANDARD_BULLET_RADIUS = 4 + private const val STANDARD_GAP_WIDTH = 2 + private const val STANDARD_COLOR = 0 + } + + private var mBulletPath: Path? = null + + override fun getLeadingMargin(first: Boolean): Int { + return 2 * bulletRadius + gapWidth + } + + override fun drawLeadingMargin( + canvas: Canvas, paint: Paint, x: Int, dir: Int, + top: Int, baseline: Int, bottom: Int, + text: CharSequence, start: Int, end: Int, + first: Boolean, + layout: Layout? + ) { + val bottom = bottom + if ((text as Spanned).getSpanStart(this) == start) { + val style = paint.style + val oldColor = paint.color + + paint.style = Paint.Style.FILL + if (color != STANDARD_COLOR) { + paint.color = color + } + + val yPosition = if (layout != null) { + val line = layout.getLineForOffset(start) + layout.getLineBaseline(line).toFloat() - bulletRadius * 2f + } else { + (top + bottom) / 2f + } + + val xPosition = (x + dir * bulletRadius).toFloat() + + if (canvas.isHardwareAccelerated) { + if (mBulletPath == null) { + mBulletPath = Path() + mBulletPath!!.addCircle(0.0f, 0.0f, bulletRadius.toFloat(), Direction.CW) + } + canvas.save() + canvas.translate(xPosition, yPosition) + canvas.drawPath(mBulletPath!!, paint) + canvas.restore() + } else { + canvas.drawCircle(xPosition, yPosition, bulletRadius.toFloat(), paint) + } + + paint.style = style + paint.color = oldColor + } + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt new file mode 100644 index 0000000..f9d8b8b --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt @@ -0,0 +1,152 @@ +package com.zebra.aidatacapturedemo.ui.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.zebra.aidatacapturedemo.data.CustomerDataGenerator +import com.zebra.aidatacapturedemo.data.ProductInfo +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import java.util.Locale + +@Composable +fun CustomerInformationScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + @Suppress("UNUSED_PARAMETER") innerPadding: PaddingValues +) { + // Generate data once + val customers = remember { CustomerDataGenerator.generateCustomers() } + + // Store in ViewModel so we can access it during scanning + LaunchedEffect(customers) { + viewModel.setAllCustomers(customers) + } + + // Process data to group by product + val productGroups = remember(customers) { + val groups = mutableMapOf>>() // Barcode to List of (ToteId, Quantity) + val productInfoMap = mutableMapOf() + + customers.forEach { customer -> + customer.products.forEach { product -> + groups.getOrPut(product.barcode) { mutableListOf() }.add(customer.id to product.quantity) + productInfoMap[product.barcode] = product + } + } + + productInfoMap.values.sortedBy { it.name }.map { it to groups[it.barcode]!! } + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFFF8F9FA)) + .padding(top = 40.dp) // Moved down to avoid being blocked + ) { + // Title with bottom border + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .drawBehind { + val borderSize = 1.dp.toPx() + val y = size.height - borderSize / 2 + drawLine( + color = Color.Black, + start = Offset(0f, y), + end = Offset(size.width, y), + strokeWidth = borderSize + ) + } + .padding(bottom = 8.dp) + ) { + Text( + text = "Product Picking List", + style = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + ) + } + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + items(productGroups) { (product, totes) -> + ProductPickingItem(product, totes) + } + + item { + Button( + onClick = { + viewModel.updatePickingFeedback(null) + navController.navigate(Screen.BarcodeScanPicking.route) + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF006D39)) + ) { + Text("Proceed to Scanning", color = Color.White) + } + } + } + } +} + +@Composable +fun ProductPickingItem(product: ProductInfo, totes: List>) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = product.name, + style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold, color = Color.Black) + ) + Text( + text = "Barcode: ${product.barcode} | Price: $${String.format(Locale.US, "%.2f", product.price)}", + style = TextStyle(fontSize = 14.sp, color = Color.Black) + ) + + Spacer(modifier = Modifier.height(8.dp)) + HorizontalDivider(color = Color.LightGray, thickness = 0.5.dp) + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Tote Distribution:", + style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.Black) + ) + + totes.forEach { (toteId, qty) -> + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = "Tote $toteId", style = TextStyle(fontSize = 14.sp, color = Color.Black)) + Text(text = "Qty: $qty", style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Color.Black)) + } + } + } + } +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt new file mode 100644 index 0000000..80fc35a --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt @@ -0,0 +1,710 @@ +package com.zebra.aidatacapturedemo.ui.view + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +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.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.NavHostController +import com.zebra.aidatacapturedemo.BuildConfig +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.UsecaseState +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +/** + * Data class representing an expandable settings item with a title and expansion state. + * + * @param title The title of the settings item. + * @param isExpanded A boolean indicating whether the item is currently expanded or not. Default is false. + */ +data class ExpandableSettingsItem( + val title: String, + var isExpanded: Boolean = false +) + +data class ExpandableSettingsItemsList( + val itemsTitle: MutableList = mutableStateListOf() +) + +@Composable +fun ExpandableSettingsItemsList.AddCommonSettings() { + itemsTitle.add(ExpandableSettingsItem(stringResource(R.string.model_input_size))) + itemsTitle.add(ExpandableSettingsItem(stringResource(R.string.resolution))) + itemsTitle.add(ExpandableSettingsItem(stringResource(R.string.runtime_processor))) +} + +@Composable +fun ExpandableSettingsItemsList.AddAboutSettings() { + itemsTitle.add(ExpandableSettingsItem(stringResource(R.string.about))) +} + +@Composable +fun DemoSettingsScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavHostController, + innerPadding: PaddingValues +) { + + val uiState = viewModel.uiState.collectAsState().value + + // Intercept back presses on this screen + val demo = uiState.usecaseSelected + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + viewModel.updateAppBarTitle(stringResource(R.string.settings)) + val settingsItemsList = ExpandableSettingsItemsList() + settingsItemsList.AddCommonSettings() + + if (demo == UsecaseState.Barcode.value || demo == UsecaseState.BarcodeMap.value) { + settingsItemsList.AddBarcodeSettings() + } + else if (demo == UsecaseState.OCRBarcodeFind.value){ + settingsItemsList.AddBarcodeSettings() + settingsItemsList.AddFeedbackSettings() + } else if (demo == UsecaseState.Retail.value) { + settingsItemsList.AddProductRecognitionSettings() + } else if (demo == UsecaseState.Product.value) { + settingsItemsList.AddProductEnrollmentSettings() + } + settingsItemsList.AddAboutSettings() + + Column( + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .background(color = Variables.surfaceDefault) + ) { + val items = remember { + List(settingsItemsList.itemsTitle.size) { index -> + ExpandableSettingsItem(settingsItemsList.itemsTitle[index].title) + } + } + val expandedStates = + remember { mutableStateListOf(*BooleanArray(items.size) { false }.toTypedArray()) } + // If user selected Go button during DemoStartScreen -> CameraPreviewScreen -> BarcodeFindFilterHomeScreen, + // now find the Barcode Symbology ExpandableSettingsItem and make the view expandable. + LaunchedEffect(key1 = "Make Barcode Symbology view expand") { + // Check if Screen.Preview exists inside the navigation Controller Stack. + if (checkIfScreenExistsInStack(navController, Screen.Preview.route)) { + expandedStates[3] = true + } + } + + val listState = rememberLazyListState() + + LazyColumn( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top), + horizontalAlignment = Alignment.Start, + contentPadding = PaddingValues(top = 16.dp, bottom = 32.dp), + state = listState + ) { + itemsIndexed(items, key = { index, _ -> index }) { index, item -> + ExpandableSettingsListItem( + item = item, + index = index, + isExpanded = expandedStates[index], + onExpandedChange = { + for (i in items.indices) { + expandedStates[i] = false + } + expandedStates[index] = it + }, + viewModel, navController + ) + } + } + Column( + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(bottom = 24.dp) + .width(312.dp) + .wrapContentHeight() + ) { + if (demo == UsecaseState.OCR.value) { + BorderlessButton( + BorderlessButtonData( + R.string.advanced_settings, + onButtonClick = { + navController.navigate(Screen.AdvancedOCRSettings.route) + } + )) + } + BorderlessButton( + BorderlessButtonData( + R.string.restore_default, + onButtonClick = { + viewModel.restoreDefaultSettings() + } + )) + } + } +} + +@Composable +fun SettingHeader(viewModel: AIDataCaptureDemoViewModel, document: Document) { + var isMoreInfoShown by remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(color = Variables.mainInverse) + ) { + Row( + modifier = Modifier.padding(start = 12.dp, top = 16.dp, end = 12.dp) + ) { + val element: Element? = document.getElementById("summary") + var infoText = AnnotatedString("") + if (element != null) { + infoText = AnnotatedString.fromHtml(element.html()) + } + Text( + text = infoText, + style = TextStyle( + fontSize = 14.sp, + lineHeight = 18.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainDefault, + ) + ) + } + Row( + modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp, bottom = 16.dp) + ) { + Text( + text = "More >", + Modifier + .clickable { + isMoreInfoShown = true + }, + style = TextStyle( + fontSize = 14.sp, + lineHeight = 18.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainPrimary, + ) + ) + } + } + if (isMoreInfoShown) { + isMoreInfoShown = SettingsMoreInfoScreen(viewModel, document, isMoreInfoShown) + } +} + +@Composable +fun ExpandableSettingsListItem( + item: ExpandableSettingsItem, + index: Int, + isExpanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + viewModel: AIDataCaptureDemoViewModel, + navController: NavController +) { + val interactionSource = remember { MutableInteractionSource() } + val rotationAngle by animateFloatAsState(targetValue = if (isExpanded) 180f else 0f) + + Column( + horizontalAlignment = Alignment.Start, + modifier = Modifier + .fillMaxWidth() + .background(color = Variables.surfaceDefault) + .clickable(interactionSource = interactionSource, indication = null) { + onExpandedChange(!isExpanded) + } + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .border(width = 1.dp, color = Variables.borderDefault) + .fillMaxWidth() + .wrapContentHeight() + .background(color = Variables.mainLight) + .padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 12.dp) + ) { + Text( + text = item.title, + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = ImageVector.Companion.vectorResource(id = R.drawable.down_arrow_icon), + contentDescription = if (isExpanded) "Collapse" else "Expand", + modifier = Modifier + .graphicsLayer(rotationZ = rotationAngle) + .padding(1.dp) + .width(20.dp) + .height(20.dp), + tint = Variables.mainSubtle + ) + } + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + AddIndividualSettings(item, viewModel) + } + } +} + +@Composable +fun AddIndividualSettings(item: ExpandableSettingsItem, viewModel: AIDataCaptureDemoViewModel) { + val uiState = viewModel.uiState.collectAsState().value + if (item.title.equals(stringResource(R.string.runtime_processor))) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val htmlString = viewModel.loadInputStreamFromAsset(fileName = "processor.html") + val document: Document = Jsoup.parse(htmlString) + SettingHeader(viewModel = viewModel, document) + AddProcessorRadioButtonList(viewModel) + } + } else if (item.title.equals(stringResource(R.string.model_input_size))) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + var fileName: String = "" + if ((uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value) || (uiState.usecaseSelected == UsecaseState.OCR.value)) { + fileName = "ocr_model_input_size.html" + } else if (uiState.usecaseSelected == UsecaseState.Barcode.value || uiState.usecaseSelected == UsecaseState.BarcodeMap.value) { + fileName = "barcode_model_input_size.html" + } else { + fileName = "product_model_input_size.html" + } + val htmlString = viewModel.loadInputStreamFromAsset(fileName = fileName) + val document: Document = Jsoup.parse(htmlString) + SettingHeader(viewModel = viewModel, document) + AddModelInputSizeRadioButtonList(viewModel) + } + } else if (item.title.equals(stringResource(R.string.resolution))) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + var fileName: String = "" + if ((uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value) || (uiState.usecaseSelected == UsecaseState.OCR.value)) { + fileName = "ocr_resolution.html" + } else if (uiState.usecaseSelected == UsecaseState.Barcode.value || uiState.usecaseSelected == UsecaseState.BarcodeMap.value) { + fileName = "barcode_resolution.html" + } else { + fileName = "product_resolution.html" + } + val htmlString = viewModel.loadInputStreamFromAsset(fileName = fileName) + val document: Document = Jsoup.parse(htmlString) + SettingHeader(viewModel = viewModel, document) + AddResolutionRadioButtonList(viewModel) + } + } else if (item.title.equals(stringResource(R.string.barcode_symbology))) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + var fileName: String = "" + if ((uiState.usecaseSelected == UsecaseState.Barcode.value) || (uiState.usecaseSelected == UsecaseState.BarcodeMap.value) || (uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value)) { + fileName = "barcode_symbologies.html" + } + val htmlString = viewModel.loadInputStreamFromAsset(fileName = fileName) + val document: Document = Jsoup.parse(htmlString) + SettingHeader(viewModel = viewModel, document) + AddBarcodeSymbologySwitchOption(viewModel) + } + } else if (item.title.equals(stringResource(R.string.detection_parameters))) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AddOCRDetectionOptions(viewModel) + } + } else if (item.title.equals(stringResource(R.string.recognition_parameters))) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AddOCRRecognitionOptions(viewModel) + } + } else if (item.title.equals(stringResource(R.string.grouping))) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AddEnableOCRGroupingOptions(viewModel) + } + } else if (item.title.equals(stringResource(R.string.import_database))) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AddImportDatabaseOptions(viewModel) + } + } else if (item.title.equals(stringResource(R.string.export_database))) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AddExportDatabaseOptions(viewModel) + } + } else if (item.title.equals(stringResource(R.string.clear_active_database))) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AddClearActiveDatabaseOptions(viewModel) + } + } else if (item.title.equals(stringResource(R.string.about))) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AddAboutInformation(viewModel) + } + } else if (item.title.equals(stringResource(R.string.similarity_threshold))) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val fileName = "product_similaritythreshold.html" + + val htmlString = viewModel.loadInputStreamFromAsset(fileName = fileName) + val document: Document = Jsoup.parse(htmlString) + SettingHeader(viewModel = viewModel, document) + AddSimilarityThreshold(viewModel) + } + } + else if (item.title.equals(stringResource(R.string.feedback))) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AddFeedbackSwitchOption(viewModel) + } + } +} + +@Composable +fun AddProcessorRadioButtonList(viewModel: AIDataCaptureDemoViewModel) { + val currentUIState = viewModel.uiState.collectAsState().value + val listOfProcessors = listOf( + RadioButtonData( + stringResource(R.string.processor_auto), + getSettingDescription( + currentUIState.usecaseSelected, + R.string.runtime_processor, + 0 + ), + 0, + onItemSelected = { selectedProcessor -> + viewModel.updateSelectedProcessor(selectedProcessor) + }), + RadioButtonData( + stringResource(R.string.processor_dsp), + getSettingDescription( + currentUIState.usecaseSelected, + R.string.runtime_processor, + 1 + ), + 1, + onItemSelected = { selectedProcessor -> + viewModel.updateSelectedProcessor(selectedProcessor) + } + ), + RadioButtonData( + stringResource(R.string.processor_gpu), + getSettingDescription( + currentUIState.usecaseSelected, + R.string.runtime_processor, + 2 + ), + 2, + onItemSelected = { selectedProcessor -> + viewModel.updateSelectedProcessor(selectedProcessor) + }), + RadioButtonData( + stringResource(R.string.processor_cpu), + getSettingDescription( + currentUIState.usecaseSelected, + R.string.runtime_processor, + 3 + ), + 3, + onItemSelected = { selectedProcessor -> + viewModel.updateSelectedProcessor(selectedProcessor) + }) + ) + viewModel.getProcessorSelectedIndex()?.let { + ListOfRadioButtonOptions(it, listOfProcessors) + } +} + +@Composable +fun AddModelInputSizeRadioButtonList(viewModel: AIDataCaptureDemoViewModel) { + val currentUIState = viewModel.uiState.collectAsState().value + val listOfModelInputSizes = mutableListOf( + RadioButtonData( + stringResource(R.string.model_input_size_640), + getSettingDescription( + currentUIState.usecaseSelected, + R.string.model_input_size, + 0 + ), + 0, + onItemSelected = { selectedModelInputSize -> + viewModel.updateSelectedDimensions(selectedModelInputSize) + }), + RadioButtonData( + stringResource(R.string.model_input_size_1280), + getSettingDescription( + currentUIState.usecaseSelected, + R.string.model_input_size, + 1 + ), + 1, + onItemSelected = { selectedModelInputSize -> + viewModel.updateSelectedDimensions(selectedModelInputSize) + }), + RadioButtonData( + stringResource(R.string.model_input_size_1600), + getSettingDescription( + currentUIState.usecaseSelected, + R.string.model_input_size, + 2 + ), + 2, + onItemSelected = { selectedModelInputSize -> + viewModel.updateSelectedDimensions(selectedModelInputSize) + } + ), + RadioButtonData( + stringResource(R.string.model_input_size_2560), + getSettingDescription( + currentUIState.usecaseSelected, + R.string.model_input_size, + 3 + ), + 3, + onItemSelected = { selectedModelInputSize -> + viewModel.updateSelectedDimensions(selectedModelInputSize) + } + ) + ) + if (currentUIState.usecaseSelected == UsecaseState.Barcode.value || currentUIState.usecaseSelected == UsecaseState.BarcodeMap.value) { + // Remove inputSize 2560 option for Barcode Decoder + listOfModelInputSizes.removeAt(listOfModelInputSizes.size - 1) + } + var selectedIndex = 0 + if (viewModel.getInputSizeSelected() == 1280) { + selectedIndex = 1 + } else if (viewModel.getInputSizeSelected() == 1600) { + selectedIndex = 2 + } else if (viewModel.getInputSizeSelected() == 2560) { + selectedIndex = 3 + } + ListOfRadioButtonOptions(selectedIndex, listOfModelInputSizes) +} + +@Composable +fun AddResolutionRadioButtonList(viewModel: AIDataCaptureDemoViewModel) { + val currentUIState = viewModel.uiState.collectAsState().value + val listOfResolutionSizes = listOf( + RadioButtonData( + stringResource(R.string.resolution_size_1280), + getSettingDescription( + currentUIState.usecaseSelected, + R.string.resolution, + 0 + + ), + 0, + onItemSelected = { selectedResolution -> + viewModel.updateSelectedResolution(selectedResolution) + }), + RadioButtonData( + stringResource(R.string.resolution_size_1920), + getSettingDescription( + currentUIState.usecaseSelected, + R.string.resolution, + 1 + ), + 1, + onItemSelected = { selectedResolution -> + viewModel.updateSelectedResolution(selectedResolution) + }), + RadioButtonData( + stringResource(R.string.resolution_size_2688), + getSettingDescription( + currentUIState.usecaseSelected, + R.string.resolution, + 2 + ), + 2, + onItemSelected = { selectedResolution -> + viewModel.updateSelectedResolution(selectedResolution) + } + ), + RadioButtonData( + stringResource(R.string.resolution_size_3840), + getSettingDescription( + currentUIState.usecaseSelected, + R.string.resolution, + 3 + ), + 3, + onItemSelected = { selectedResolution -> + viewModel.updateSelectedResolution(selectedResolution) + } + ) + ) + viewModel.getSelectedResolution()?.let { + ListOfRadioButtonOptions(it, listOfResolutionSizes) + } +} + +@Composable +fun AddAboutInformation(viewModel: AIDataCaptureDemoViewModel) { + val uiState = viewModel.uiState.collectAsState().value + val versionPair = when (uiState.usecaseSelected) { + UsecaseState.OCRBarcodeFind.value -> { + Pair(first = "OCR Barcode Find Version", second = BuildConfig.TextOcrRecognizer_Version) + } + + UsecaseState.OCR.value -> { + Pair( + first = "Text/Ocr Recognizer Version", + second = BuildConfig.TextOcrRecognizer_Version + ) + } + + UsecaseState.Barcode.value, + UsecaseState.BarcodeMap.value -> { + Pair( + first = "Barcode Recognizer Version", + second = BuildConfig.BarcodeLocalizer_Version + ) + } + + UsecaseState.Product.value -> { + Pair( + first = "Product & Shelf Enrollment Version", + second = BuildConfig.ProductAndShelfRecognizer_Version + ) + } + + UsecaseState.Retail.value -> { + Pair( + first = "Product & Shelf Recognizer Version", + second = BuildConfig.ProductAndShelfRecognizer_Version + ) + } + + else -> { + TODO("On AddAboutInformation() - Invalid use case ${uiState.usecaseSelected} selected") + } + } + + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = versionPair.first, + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Color(0xFF1D1E23), + ), + modifier = Modifier.padding(top = 18.dp, bottom = 6.dp, start = 14.4.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = versionPair.second, + + style = TextStyle( + fontSize = 12.sp, + lineHeight = 16.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Color(0xFF646A78), + textAlign = TextAlign.Right, + ), + modifier = Modifier.padding(end = 22.dp, top = 14.dp) + ) + } +} + +fun checkIfScreenExistsInStack(navController: NavHostController, targetRoute: String): Boolean { + return try { + navController.getBackStackEntry(targetRoute) + true + } catch (_: IllegalArgumentException) { + false + } +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoStartScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoStartScreen.kt new file mode 100644 index 0000000..963651a --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoStartScreen.kt @@ -0,0 +1,715 @@ +package com.zebra.aidatacapturedemo.ui.view + +import android.content.Context +import android.view.WindowManager +import android.view.WindowMetrics +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonColors +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.core.content.ContextCompat.getSystemService +import androidx.navigation.NavController +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.UsecaseState +import com.zebra.aidatacapturedemo.ui.view.Variables.mainDisabled +import com.zebra.aidatacapturedemo.ui.view.Variables.mainPrimary +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +/* + * DemoStartScreen is the initial screen of the AI Data Capture Demo app, providing users with an + * overview of the selected use case and its settings before starting the scanning process. + * It displays relevant information such as model input size, resolution, and inference type + * based on the user's selections. The screen also includes a loading overlay while models are + * being initialized and handles back button presses to ensure proper navigation flow. + */ +@Composable +fun DemoStartScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + innerPadding: PaddingValues, + context: Context +) { + var isStartDisabled = remember { mutableStateOf(true) } + + val uiState = viewModel.uiState.collectAsState().value + getDemoTitle(uiState.usecaseSelected)?.let { viewModel.updateAppBarTitle(stringResource(it)) } + + + uiState.toastMessage?.let { + viewModel.toast(it) + viewModel.updateToastMessage(message = null) + } + // Intercept back presses on this screen + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + LoadingScreen(viewModel, navController, uiState, isStartDisabledChanged = {isStartDisabled.value = it}) + val windowManager = getSystemService(context, WindowManager::class.java) + val windowMetrics: WindowMetrics = windowManager.currentWindowMetrics + // draw smaller icon if device display height is 800px or less + if (windowMetrics.bounds.height() <= 800) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .background(color = Variables.surfaceDefault) + .fillMaxWidth() + .fillMaxHeight() + .padding(innerPadding) + ) { + + // Icon + Spacer(Modifier.height(10.dp)) + + UsecaseIcon(selectedUsecase = uiState.usecaseSelected) + + Spacer(Modifier.height(10.dp)) + Column( + modifier = Modifier + .wrapContentHeight() + .padding(start = 16.dp, end = 16.dp) + ) { + // Heading: + val titleStringId = getSettingHeading(uiState.usecaseSelected) + if (titleStringId == null) { + TextviewBold(info = "") + } else { + TextviewBold(info = stringResource(titleStringId)) + } + + // Model Input Details: + Row { + viewModel.getInputSizeSelected()?.let { + SmallScreenTextviewNormal(info = "\u2022 Model Input:") + SmallScreenTextviewNormal(info = " $it x $it") + } + } + + // Resolution Details: + Row { + viewModel.getSelectedResolution()?.let { + SmallScreenTextviewNormal(info = "\u2022 Resolution:") + val resolution = getSelectedResolution(it) + SmallScreenTextviewNormal(info = " $resolution") + } + } + + // Inference Type Details: + Row { + viewModel.getProcessorSelectedIndex()?.let { + SmallScreenTextviewNormal(info = "\u2022 Inference (processor) Type:") + val inferenceType = getSelectedInferenceType(it) + SmallScreenTextviewNormal(info = " $inferenceType") + } + } + + if (uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value) { + Spacer(modifier = Modifier.height(4.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(0.dp, Alignment.Start), + verticalAlignment = Alignment.Top, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(color = Variables.colorsSurfaceDisabled, shape = RoundedCornerShape(size = Variables.radiusMinimal)) + .padding(horizontal = Variables.spacingMinimum) + ) { + SingleChoiceSegmentedButton(viewModel,uiState.isCaptureOrLiveEnabled) + } + + // Barcode Switch + Spacer(modifier = Modifier.height(4.dp)) + Row { + SwitchOptionForModelSelectionScreen( + uiState.isBarcodeModelEnabled, + SwitchOptionData( + R.string.barcode_model, + onItemSelected = { title, enabled -> + viewModel.updateBarcodeModelEnabled(enabled) + viewModel.deinitModel() + viewModel.initModel() + }) + ) + } + Spacer(modifier = Modifier.width(4.dp)) + Row { + SwitchOptionForModelSelectionScreen( + uiState.isOCRModelEnabled, + SwitchOptionData( + R.string.ocr_model, + onItemSelected = { title, enabled -> + viewModel.updateOCRModelEnabled(enabled) + viewModel.deinitModel() + viewModel.initModel() + }) + ) + } + + // Restore Clickable Text: + Text( + text = stringResource(R.string.restore_default), + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .clickable { + viewModel.restoreDefaultSettings() + viewModel.applySettings() + }, + style = TextStyle( + fontSize = 12.sp, + lineHeight = 16.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Variables.borderPrimaryMain, + ) + ) + }else{ + Spacer(modifier = Modifier.height(30.dp)) + + // Restore Clickable Text: + Text( + text = stringResource(R.string.restore_default), + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .clickable { + viewModel.restoreDefaultSettings() + viewModel.applySettings() + }, + style = TextStyle( + fontSize = 14.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Variables.borderPrimaryMain, + ) + ) + } + + } + + Row( + modifier = Modifier + .fillMaxHeight() + .padding(horizontal = 24.dp), + verticalAlignment = Alignment.Bottom + ) { + if(isStartDisabled.value == true){ + SmallScreenButtonOption( + ButtonData( + R.string.start_scan, + mainDisabled, + 1.0F, + false, + onButtonClick = { + }) + ) + } else { + SmallScreenButtonOption( + ButtonData( + R.string.start_scan, + mainPrimary, + 1.0F, + true, + onButtonClick = { + navController.navigate(route = Screen.Preview.route) + }) + ) + } + } + } + + } else { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .background(color = Variables.surfaceDefault) + .fillMaxWidth() + .fillMaxHeight() + .padding(innerPadding) + ) { + + // Icon + Spacer(Modifier.height(37.dp)) + + UsecaseIcon(selectedUsecase = uiState.usecaseSelected) + + Spacer(Modifier.height(48.dp)) + Column( + modifier = Modifier + .wrapContentHeight() + .padding(start = 16.dp, end = 16.dp) + ) { + // Heading: + val titleStringId = getSettingHeading(uiState.usecaseSelected) + if (titleStringId == null) { + TextviewBold(info = "") + } else { + TextviewBold(info = stringResource(titleStringId)) + } + + // Model Input Details: + Spacer(modifier = Modifier.height(8.dp)) + Row { + viewModel.getInputSizeSelected()?.let { + TextviewBold(info = "\u2022 Model Input:") + TextviewNormal(info = " $it x $it") + } + } + + // Resolution Details: + Spacer(modifier = Modifier.height(4.dp)) + Row { + viewModel.getSelectedResolution()?.let { + TextviewBold(info = "\u2022 Resolution:") + val resolution = getSelectedResolution(it) + TextviewNormal(info = " $resolution") + } + } + + // Inference Type Details: + Spacer(modifier = Modifier.height(4.dp)) + Row { + viewModel.getProcessorSelectedIndex()?.let { + TextviewBold(info = "\u2022 Inference (processor) Type:") + val inferenceType = getSelectedInferenceType(it) + TextviewNormal(info = " $inferenceType") + } + } + + if (uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value) { + Spacer(modifier = Modifier.height(12.dp)) + TextviewBold(info = "Select Capture Setup") + Spacer(modifier = Modifier.height(12.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(0.dp, Alignment.Start), + verticalAlignment = Alignment.Top, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(color = Variables.colorsSurfaceDisabled, shape = RoundedCornerShape(size = Variables.radiusMinimal)) + .padding(horizontal = Variables.spacingMinimum) + ) { + SingleChoiceSegmentedButton(viewModel,uiState.isCaptureOrLiveEnabled) + } + // Barcode Switch + Spacer(modifier = Modifier.height(12.dp)) + Row { + SwitchOptionForModelSelectionScreen( + uiState.isBarcodeModelEnabled, + SwitchOptionData( + R.string.barcode_model, + onItemSelected = { title, enabled -> + viewModel.updateBarcodeModelEnabled(enabled) + viewModel.deinitModel() + viewModel.initModel() + }) + ) + } + Spacer(modifier = Modifier.height(12.dp)) + Row { + SwitchOptionForModelSelectionScreen( + uiState.isOCRModelEnabled, + SwitchOptionData( + R.string.ocr_model, + onItemSelected = { title, enabled -> + viewModel.updateOCRModelEnabled(enabled) + viewModel.deinitModel() + viewModel.initModel() + }) + ) + } + } + // Restore Clickable Text: + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = stringResource(R.string.restore_default), + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .clickable { + viewModel.restoreDefaultSettings() + viewModel.applySettings() + }, + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Variables.borderPrimaryMain, + ) + ) + } + + Row( + modifier = Modifier + .fillMaxHeight() + .padding(bottom = 24.dp), + verticalAlignment = Alignment.Bottom + ) { + if(isStartDisabled.value == true){ + ButtonOption( + ButtonData( + R.string.start_scan, + mainDisabled, + 1.0F, + false, + onButtonClick = { + }) + ) + } else { + ButtonOption( + ButtonData( + R.string.start_scan, + mainPrimary, + 1.0F, + true, + onButtonClick = { + navController.navigate(route = Screen.Preview.route) + }) + ) + } + } + } + } +} + +@Composable +private fun getSelectedInferenceType(processorSelectedIndex: Int): String { + return when (processorSelectedIndex) { + 0 -> { + stringResource(R.string.processor_auto) + } + + 1 -> { + stringResource(R.string.processor_dsp_short) + } + + 2 -> { + stringResource(R.string.processor_gpu_short) + } + + 3 -> { + stringResource(R.string.processor_cpu_short) + } + + else -> { + stringResource(R.string.processor_auto) + } + } +} + +@Composable +private fun getSelectedResolution(resolutionSelectedIndex: Int): String { + return when (resolutionSelectedIndex) { + 0 -> { + "${stringResource(R.string.resolution_size_1280)}" + } + + 1 -> { + "${stringResource(R.string.resolution_size_1920)}" + } + + 2 -> { + "${stringResource(R.string.resolution_size_2688)}" + } + + 3 -> { + "${stringResource(R.string.resolution_size_3840)}" + } + + else -> { + TODO("Unknown Resolution found $resolutionSelectedIndex") + } + } +} + +@Composable +fun UsecaseIcon(selectedUsecase: String) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .width(88.dp) + .height(88.dp) + .background( + shape = RoundedCornerShape( + topStart = 6.dp, + topEnd = 6.dp, + bottomStart = 6.dp, + bottomEnd = 6.dp + ), + brush = Brush.verticalGradient( + colors = listOf( + getIconMainColor(selectedUsecase), + getIconSecondaryColor(selectedUsecase) + ) + ) + ) + ) { + getIconId(selectedUsecase)?.let { + Image( + painter = painterResource(id = it), + contentDescription = "image description", + contentScale = ContentScale.Fit, + modifier = Modifier + .width(64.dp) + .height(64.dp) + ) + } + } +} + +@Composable +fun ModalLoadingOverlay(onDismissRequest: () -> Unit) { + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, // Crucial for full width + decorFitsSystemWindows = false // Allows drawing under system bars if configured in Activity + ) + ) { + // Block user interaction with the UI below the overlay + Box( + modifier = Modifier + .wrapContentSize() + .background(Color.White.copy(alpha = 0.0f)) // Semi-transparent background + .pointerInput(Unit) { + // Intercept all tap gestures so they don't reach the underlying content + detectTapGestures(onTap = { /* Do nothing */ }) + }, + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .width(164.dp) + .height(164.dp) + .background( + color = Variables.surfaceDefault, + shape = RoundedCornerShape(size = 8.dp) + ) + .border( + width = 1.dp, + color = Variables.borderDefault, + shape = RoundedCornerShape(size = 8.dp) + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(33.dp)) + CircularProgressIndicator( + color = mainPrimary, + modifier = Modifier + .width(56.dp) + .height(56.dp), + strokeWidth = 7.dp + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = "Loading", + style = TextStyle( + fontSize = 14.sp, + lineHeight = 18.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsMainSubtle, + textAlign = TextAlign.Center, + ) + ) + } + } + + // Handle the back button press to prevent dismissal during critical ops + BackHandler { + onDismissRequest() + } + } +} + +@Composable +fun SingleChoiceSegmentedButton(viewModel: AIDataCaptureDemoViewModel, currentChoice : Int, modifier: Modifier = Modifier) { + var selectedIndex = remember { mutableIntStateOf(currentChoice) } + val options = listOf("Image Capture", "Live Video") + val icons = listOf(R.drawable.camera_icon, R.drawable.video_icon) + + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + options.forEachIndexed { index, label -> + SegmentedButton( + shape = RoundedCornerShape(size = Variables.radiusMinimal), + colors = SegmentedButtonColors( + Variables.surfaceDefault, + mainPrimary, + Variables.surfaceDefault, + inactiveContainerColor = Variables.colorsSurfaceDisabled, + inactiveContentColor = Variables.colorsMainSubtle, + inactiveBorderColor = Variables.colorsSurfaceDisabled, + disabledActiveContainerColor = Variables.colorsSurfaceDisabled, + disabledActiveContentColor = Variables.colorsMainSubtle, + disabledActiveBorderColor = Variables.colorsSurfaceDisabled, + disabledInactiveContainerColor = Variables.colorsSurfaceDisabled, + disabledInactiveContentColor = Variables.colorsMainSubtle, + disabledInactiveBorderColor = Variables.colorsSurfaceDisabled, + ), + modifier = Modifier.weight(1f), + onClick = { + selectedIndex.value = index + viewModel.updateCaptureOrLiveEnabled(index) + viewModel.deinitModel() + viewModel.initModel() + }, + selected = index == selectedIndex.value, + label = { + Text( + text = label, + style = TextStyle( + fontSize = Variables.TypefaceFontSize14, + lineHeight = Variables.TypefaceLineHeight20, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = if (index == selectedIndex.value){ + Variables.colorsBorderPrimaryLegacy + }else{ + Variables.colorsTextDefault + }, + textAlign = TextAlign.Center, + ) + ) + }, + icon = { Icon( + painter = painterResource(id = icons[index]), + contentDescription = "Camera Icon", + ) } + ) + } + } +} + +@Composable +private fun LoadingScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + uiState: AIDataCaptureDemoUiState, + isStartDisabledChanged: (Boolean) -> Unit, +) { + var isLoading = remember { mutableStateOf(true) } + when (uiState.usecaseSelected) { + UsecaseState.OCRBarcodeFind.value -> { + if (uiState.isBarcodeModelEnabled && uiState.isOCRModelEnabled) { + if(uiState.isBarcodeModelDemoReady && uiState.isOcrModelDemoReady) { + isLoading.value = false + isStartDisabledChanged(false) + } else { + isLoading.value = true + isStartDisabledChanged(true) + } + } + else if (!uiState.isBarcodeModelEnabled && !uiState.isOCRModelEnabled) { + isLoading.value = false + isStartDisabledChanged(true) + } else if (uiState.isBarcodeModelEnabled && !uiState.isOCRModelEnabled) { + if(uiState.isBarcodeModelDemoReady) { + isLoading.value = false + isStartDisabledChanged(false) + } else { + isLoading.value = true + isStartDisabledChanged(true) + } + } else if (!uiState.isBarcodeModelEnabled && uiState.isOCRModelEnabled) { + if(uiState.isOcrModelDemoReady) { + isLoading.value = false + isStartDisabledChanged(false) + } else { + isLoading.value = true + isStartDisabledChanged(true) + } + } else { + isLoading.value = false + isStartDisabledChanged(true) + } + } + UsecaseState.Barcode.value, + UsecaseState.BarcodeMap.value -> { + if (uiState.isBarcodeModelDemoReady) { + isLoading.value = false + isStartDisabledChanged(false) + } else { + isLoading.value = true + isStartDisabledChanged(true) + } + } + UsecaseState.OCR.value -> { + if (uiState.isOcrModelDemoReady) { + isLoading.value = false + isStartDisabledChanged(false) + } else { + isLoading.value = true + isStartDisabledChanged(true) + } + } + UsecaseState.Retail.value, + UsecaseState.Product.value -> { + if (uiState.isRetailShelfModelDemoReady) { + isLoading.value = false + isStartDisabledChanged(false) + } else { + isLoading.value = true + isStartDisabledChanged(true) + } + } + } + if(isLoading.value == true) { + ModalLoadingOverlay( + onDismissRequest = { + viewModel.handleBackButton(navController = navController) + } + ) + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/FeedbackUtils.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/FeedbackUtils.kt new file mode 100644 index 0000000..e190dff --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/FeedbackUtils.kt @@ -0,0 +1,137 @@ +package com.zebra.aidatacapturedemo.ui.view + +import android.content.Context +import android.content.Intent +import android.media.AudioManager +import android.media.ToneGenerator +import android.os.Bundle +import android.os.VibrationEffect +import android.os.Vibrator +import android.speech.RecognitionListener +import android.speech.RecognizerIntent +import android.speech.SpeechRecognizer +import android.util.Log +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.AdvancedFilterOption +import com.zebra.aidatacapturedemo.data.CharacterMatchFilterOption +import com.zebra.aidatacapturedemo.data.DetectionLevel +import com.zebra.aidatacapturedemo.data.OcrRegularFilterOption +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +/** + * FeedbackUtils is a utility class that provides feedback mechanisms such as vibration, sound, + * and speech recognition for the AI Data Capture Demo. + * It initializes the necessary components for these feedback mechanisms and handles the speech + * recognition results to update the OCR filter data in the ViewModel. + */ +class FeedbackUtils(val viewModel: AIDataCaptureDemoViewModel, context: Context) { + init { + vibrator = context.getSystemService(Vibrator::class.java) + toneGenerator = ToneGenerator(AudioManager.STREAM_MUSIC, 100) // STREAM_MUSIC for general media, 100 for max volume + speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context) + speechRecognizer.setRecognitionListener(object : RecognitionListener { + override fun onResults(results: Bundle?) { + val matches = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) + if (!matches.isNullOrEmpty()) { + // Fetch the existing OcrFilterData + val defaultOcrFilterData = uiState.ocrFilterData + + // For WORD Level filters: + // Remove all hypen "-" from the resultant text if, anything exists. + val exactMatchStringList = + if (defaultOcrFilterData.selectedCharacterMatchFilterData.detectionLevel == DetectionLevel.WORD) { + matches[0]?.let { it -> + it.replace("-", "") + .replace(" ", ",") // Note: During WORD_LEVEL selection, the user can input multiple word, hence separate them using commas. + .split(",").map { it.trim() } + } ?: run { + listOf() + } + } else { // line level + matches[0]?.let { + listOf(it) // Note: During LINE_LEVEL selection, the user cannot input multiple lines. + } ?: run { + listOf() + } + } + + // Now, assign SpeechRecognizer words for Exact Match + defaultOcrFilterData.selectedCharacterMatchFilterData.type = CharacterMatchFilterOption.EXACT_MATCH + defaultOcrFilterData.selectedCharacterMatchFilterData.exactMatchStringList = exactMatchStringList + defaultOcrFilterData.selectedRegularFilterOption = OcrRegularFilterOption.ADVANCED + if (!defaultOcrFilterData.selectedAdvancedFilterOptionList.contains( + AdvancedFilterOption.CHARACTER_MATCH)) { + defaultOcrFilterData.selectedAdvancedFilterOptionList.add(AdvancedFilterOption.CHARACTER_MATCH) + } + viewModel.updateOcrFilterData(ocrFilterData = defaultOcrFilterData) + + micStatePressed = false + Log.d("SpeechRecognizer", "onResults: $exactMatchStringList" ); + } + } + override fun onReadyForSpeech(params: Bundle?) { + Log.d("SpeechRecognizer", "onReadyForSpeech"); + } + override fun onBeginningOfSpeech() { + Log.d("SpeechRecognizer", "onBeginningOfSpeech"); + } + override fun onBufferReceived(p0: ByteArray?) { + Log.d("SpeechRecognizer", "onBufferReceived"); + } + override fun onEndOfSpeech() { + Log.d("SpeechRecognizer", "onEndOfSpeech"); + micStatePressed = false + } + override fun onRmsChanged(p0: Float) { + + } + override fun onError(error: Int) { + Log.d("SpeechRecognizer", "onError : ${error}"); + micStatePressed = false + } + override fun onEvent(p0: Int, p1: Bundle?) { + Log.d("SpeechRecognizer", "onEvent : ${p0}"); + } + + override fun onPartialResults(p0: Bundle?) { + Log.d("SpeechRecognizer", "onPartialResults : ${p0}"); + } + // ... other RecognitionListener methods + }) + } + companion object { + private lateinit var uiState: AIDataCaptureDemoUiState + var micStatePressed: Boolean = false + private lateinit var vibrator: Vibrator + private lateinit var toneGenerator : ToneGenerator + private lateinit var speechRecognizer: SpeechRecognizer + private val speechRecognitionIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { + putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) + putExtra(RecognizerIntent.EXTRA_LANGUAGE, "en-US") + putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak now...") + } + fun vibrate() { + if (vibrator != null && vibrator.hasVibrator()) { + vibrator.vibrate(VibrationEffect.createOneShot(150, VibrationEffect.DEFAULT_AMPLITUDE)) + } + } + fun beep() { + toneGenerator.startTone(ToneGenerator.TONE_CDMA_PIP, 150) // TONE_CDMA_PIP for a short "pip" sound, 150ms duration + } + + fun startListening(uiState: AIDataCaptureDemoUiState) { + Companion.uiState = uiState + speechRecognizer.startListening(speechRecognitionIntent) + } + + fun stopListening() { + speechRecognizer.cancel() + } + + fun deinitialize() { + toneGenerator.release() + speechRecognizer.destroy() + } + + } +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt new file mode 100644 index 0000000..4b8fa62 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt @@ -0,0 +1,362 @@ +package com.zebra.aidatacapturedemo.ui.view + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.UsecaseState +import com.zebra.aidatacapturedemo.ui.view.Variables.mainIcon1 +import com.zebra.aidatacapturedemo.ui.view.Variables.mainIcon2 +import com.zebra.aidatacapturedemo.ui.view.Variables.secondaryIcon1 +import com.zebra.aidatacapturedemo.ui.view.Variables.secondaryIcon2 +/** + * Variables is an object that holds constant values for colors, dimensions, and text styles + * used throughout the AI Data Capture Demo app. + * It centralizes the design tokens to maintain consistency in the UI and allows for + * easy updates to the app's visual elements. + */ +object Variables { + val surfaceDefault: Color = Color(0xFFFFFFFF) + val mainDefault: Color = Color(0xFF1D1E23) + val mainPrimary: Color = Color(0xFF0073E6) + val mainSubtle: Color = Color(0xFF545963) + val mainIcon1: Color = Color(0xFF7E0CFF) + val secondaryIcon1: Color = Color(0xFFE600E6) + val mainIcon2: Color = Color(0xFF7E0CFF) + val secondaryIcon2: Color = Color(0xFF3F40F3) + val borderDefault: Color = Color(0xFFCED2DB) + val mainInverse: Color = Color(0xFFF3F6FA) + val warningColor: Color = Color(0xCCFFCB00) + val warningBorder: Color = Color(0xFFDDB000) + val mainDisabled: Color = Color(0xFF8D95A3) + val stateDefaultEnabled: Color = Color(0xFFFFFFFF) + val borderPrimaryMain: Color = Color(0xFF0073E6) + val uncheckedThumbColor: Color = Color(0xFFAFB6C2) + val uncheckedTrackColor: Color = Color(0xFFE8EBF1) + val colorsSurfaceDisabled: Color = Color(0xFFE0E3E9) + + val blackText: Color = Color(0xFF000000) + val spacingSmall: Dp = 8.dp + val spacingMinimum: Dp = 4.dp + + val spacingLarge: Dp = 16.dp + val spacingMedium: Dp = 12.dp + + val surfaceTertiary: Color = Color(0xFF151519) + val surfaceTertiarySelected: Color = Color(0xFF3C414B) + val mainLight: Color = Color(0xFFE0E3E9) + val inverseDefault: Color = Color(0xFFFFFFFF) + val textSubtle: Color = Color(0xFF646A78) + val borderSubtle: Color = Color(0xFFE0E3E9) + val colorsBorderPrimaryLegacy: Color = Color(0xFF3886FF) + val radiusMinimal: Dp = 4.dp + + val colorsSurfaceCool: Color = Color(0xFFF8FBFF) + + val TypefaceFontSize12: TextUnit = 12.sp + val TypefaceLineHeight16: TextUnit = 16.sp + val colorsTextDefault: Color = Color(0xFF1D1E23) + + val TypefaceLineHeight24: TextUnit = 24.sp + val colorsTextBody: Color = Color(0xFF545963) + + val colorsMainSubtle: Color = Color(0xFF545963) + + val colorsMainLight: Color = Color(0xFFE0E3E9) + + val colorsSurfacePrimary: Color = Color(0xFF1F69FF) + + val backgroundDark : Color = Color(0xBF1D1E23) + + val TypefaceFontSize14 = 14.sp + val TypefaceFontSize16 = 16.sp + val TypefaceLineHeight18 = 18.sp + val TypefaceLineHeight20 = 20.sp + + val TypefaceLetterSpacingTitle = 0.38.sp + val colorsSurfaceSelected: Color = Color(0xFFF1F8FF) + val spacingNone: Dp = 0.dp + val radiusRounded: Dp = 8.dp + + val colorsIconNegative: Color = Color(0xFFF36170) + + val colorsMainNegative: Color = Color(0xFFD70015) +} + +fun getIconMainColor(demo: String): Color { + var mainColor: Color = mainIcon2 + when (demo) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + mainColor = mainIcon2 + } + + UsecaseState.OCR.value -> { + mainColor = mainIcon2 + } + + UsecaseState.Retail.value -> { + mainColor = mainIcon2 + } + + UsecaseState.OCRBarcodeFind.value -> { + mainColor = mainIcon1 + } + + UsecaseState.Product.value -> { + mainColor = mainIcon1 + } + } + return mainColor +} + + +fun getIconSecondaryColor(demo: String): Color { + var secondaryColor: Color = secondaryIcon2 + when (demo) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + secondaryColor = secondaryIcon2 + } + + UsecaseState.OCR.value -> { + secondaryColor = secondaryIcon2 + } + + UsecaseState.Retail.value -> { + secondaryColor = secondaryIcon2 + } + + UsecaseState.OCRBarcodeFind.value -> { + secondaryColor = secondaryIcon1 + } + + UsecaseState.Product.value -> { + secondaryColor = secondaryIcon1 + } + } + return secondaryColor +} + +fun getIconId(demo: String): Int? { + var iconId: Int? = null + when (demo) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + iconId = R.drawable.barcode_icon + } + + UsecaseState.OCR.value -> { + iconId = R.drawable.ocr_icon + } + + UsecaseState.Retail.value -> { + iconId = R.drawable.retail_shelf_icon + } + + UsecaseState.OCRBarcodeFind.value -> { + iconId = R.drawable.ocr_finder_icon + } + + UsecaseState.Product.value -> { + iconId = R.drawable.product_enrollment_recognition_icon + } + } + return iconId +} + +fun getSettingHeading(demo: String): Int? { + var settingsString: Int? = null + when (demo) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + settingsString = R.string.barcode_settings + } + + UsecaseState.OCR.value -> { + settingsString = R.string.text_ocr_recognizer_settings + } + + UsecaseState.Retail.value -> { + settingsString = R.string.retailshelf_settings + } + + UsecaseState.OCRBarcodeFind.value -> { + settingsString = R.string.ocr_barcode_find_settings + } + + UsecaseState.Product.value -> { + settingsString = R.string.productrecognition_settings + } + } + return settingsString +} + +fun getDemoTitle(demo: String): Int? { + var settingsString: Int? = null + when (demo) { + UsecaseState.Barcode.value -> { + settingsString = R.string.barcode_demo + } + + UsecaseState.BarcodeMap.value -> { + settingsString = R.string.barcode_map_demo + } + + UsecaseState.OCR.value -> { + settingsString = R.string.ocr_demo + } + + UsecaseState.Retail.value -> { + settingsString = R.string.retail_shelf_demo + } + + UsecaseState.OCRBarcodeFind.value -> { + settingsString = R.string.ocr_barcode_find + } + + UsecaseState.Product.value -> { + settingsString = R.string.product_enrollment_recognition_demo + } + + UsecaseState.Main.value -> { + settingsString = R.string.app_name + } + } + return settingsString +} + +fun getSettingDescription(demo: String, setting: Int, value: Int): Int? { + var descString: Int? = null + when (setting) { + R.string.runtime_processor -> { + when (value) { + 0 -> { + descString = R.string.runtime_processor_auto_desc + } + + 1 -> { + descString = R.string.runtime_processor_dsp_desc + } + + 2 -> { + descString = R.string.runtime_processor_gpu_desc + } + + 3 -> { + descString = R.string.runtime_processor_cpu_desc + } + } + } + + R.string.resolution -> { + when (demo) { + UsecaseState.Barcode.value, + UsecaseState.BarcodeMap.value -> { + when (value) { + 0 -> { + descString = R.string.resolution_1mp_desc_bc + } + + 1 -> { + descString = R.string.resolution_2mp_desc_bc + } + + 2 -> { + descString = R.string.resolution_4mp_desc_bc + } + + 3 -> { + descString = R.string.resolution_8mp_desc_bc + } + } + } + + UsecaseState.OCR.value, + UsecaseState.OCRBarcodeFind.value -> { + when (value) { + 0 -> { + descString = R.string.resolution_1mp_desc_ocr + } + + 1 -> { + descString = R.string.resolution_2mp_desc_ocr + } + + 2 -> { + descString = R.string.resolution_4mp_desc_ocr + } + + 3 -> { + descString = R.string.resolution_8mp_desc_ocr + } + } + } + + UsecaseState.Retail.value, + UsecaseState.Product.value -> { + descString = R.string.retailshelf_settings + } + } + } + + R.string.model_input_size -> { + when (demo) { + UsecaseState.Barcode.value, + UsecaseState.BarcodeMap.value -> { + when (value) { + 0 -> { + descString = R.string.model_input_size_640_bc + } + + 1 -> { + descString = R.string.model_input_size_1280_bc + } + + 2 -> { + descString = R.string.model_input_size_1600_bc + } + + 3 -> { + descString = R.string.model_input_size_2560_bc + } + } + } + + UsecaseState.OCR.value, + UsecaseState.OCRBarcodeFind.value -> { + when (value) { + 0 -> { + descString = R.string.model_input_size_640_ocr + } + + 1 -> { + descString = R.string.model_input_size_1280_ocr + } + + 2 -> { + descString = R.string.model_input_size_1600_ocr + } + + 3 -> { + descString = R.string.model_input_size_2560_ocr + } + } + } + + UsecaseState.Retail.value, + UsecaseState.Product.value -> { + descString = R.string.retailshelf_settings + } + } + } + } + return descString +} + +object RegexConstant { + val ALPHA_ONLY = Regex(pattern = "^[A-Za-z]+\$") + val NUMERIC_ONLY = Regex(pattern = "^[0-9]*\$") + val SPECIAL_CHARACTERS_ONLY = Regex(pattern = "[^a-zA-Z0-9 ]+") + val ALPHA_AND_NUMERIC_ONLY = Regex(pattern = "^[A-Za-z0-9]+\$") + val ALPHA_AND_SPECIAL_CHARACTERS_ONLY = Regex(pattern = "[^0-9 ]+") + val NUMERIC_AND_SPECIAL_CHARACTERS_ONLY = Regex(pattern = "[^A-Za-z ]+") +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt new file mode 100644 index 0000000..04ad962 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt @@ -0,0 +1,215 @@ +package com.zebra.aidatacapturedemo.ui.view + +import android.content.Context +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.lifecycle.Lifecycle +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.zebra.aidatacapturedemo.data.FilterType +import com.zebra.aidatacapturedemo.ui.view.filters.BarcodeFindFilterHomeScreen +import com.zebra.aidatacapturedemo.ui.view.filters.CharacterMatchFilterScreen +import com.zebra.aidatacapturedemo.ui.view.filters.CharacterTypeFilterScreen +import com.zebra.aidatacapturedemo.ui.view.filters.OCRFindFilterHomeScreen +import com.zebra.aidatacapturedemo.ui.view.filters.RegexFilterScreen +import com.zebra.aidatacapturedemo.ui.view.filters.StringLengthFilterScreen +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +/** + * Screen is a sealed class that defines the different routes for navigation in the + * AI Data Capture Demo app. Each object within the sealed class represents a specific screen + * in the app, identified by a unique route string. + * This structure allows for type-safe navigation and easy management of the app's screens. + */ +sealed class Screen(val route: String) { + object Start : Screen("start_screen") + object DemoStart : Screen("demo_start_screen") + object DemoSetting : Screen("demo_setting_screen") + object DemoSettingMore : Screen("demo_setting_more_screen") + object AdvancedOCRSettings : Screen("advanced_ocr_setting_screen") + object Preview : Screen("preview_screen") + object ProductsCapture : Screen("products_capture_screen") + object OCRBarcodeCapture : Screen("ocrbarcode_capture_screen") + object OCRBarcodeResults : Screen("ocrbarcode_results_screen") + object BarcodeMapResults : Screen("barcode_map_results_screen") + object CustomerInformation : Screen("customer_information_screen") + object BarcodeScanPicking : Screen("barcode_scan_picking_screen") + object BarcodeMapPicking : Screen("barcode_map_picking_screen") + object SingleResult : Screen("single_result_screen") + + /** + * Filter related Screen + */ + object OCRFindFilterHome : Screen("ocr_find_filter_home_screen") + object CharacterTypeFilter : Screen("character_type_filter_screen") + object CharacterMatchFilter : Screen("character_match_filter_screen") + object StringLengthFilter : Screen("string_length_filter_screen") + object RegexFilter : Screen("regex_filter_screen") + object BarcodeFindFilterHome : Screen("barcode_find_filter_home_screen") +} + +@Composable +fun NavigationStack( + navController: NavHostController, + viewModel: AIDataCaptureDemoViewModel, + activityInnerPadding: PaddingValues, + innerPadding: PaddingValues, + context: Context, + activityLifecycle: Lifecycle +) { + + NavHost(navController = navController, startDestination = Screen.Start.route) { + composable(route = Screen.Start.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.Start) + AIDataCaptureStartScreen(viewModel, navController = navController, innerPadding) + } + composable(route = Screen.DemoStart.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.DemoStart) + DemoStartScreen(viewModel, navController = navController, innerPadding, context = context) + } + composable(route = Screen.DemoSetting.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.DemoSetting) + DemoSettingsScreen(viewModel, navController = navController, innerPadding) + } + composable(route = Screen.AdvancedOCRSettings.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.AdvancedOCRSettings) + AdvancedOCRSettingsScreen( + viewModel, + navController = navController, + innerPadding, + context = context + ) + } + composable(route = Screen.Preview.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.Preview) + CameraPreviewScreen( + viewModel, + navController = navController, + context, + activityInnerPadding, + activityLifecycle + ) + } + composable(route = Screen.ProductsCapture.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.ProductsCapture) + ProductsResultCapturedScreen( + viewModel, + navController = navController, + innerPadding, + context = context + ) + } + composable(route = Screen.OCRBarcodeCapture.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.OCRBarcodeCapture) + OCRBarcodeResultCapturedScreen( + viewModel, + navController = navController, + innerPadding, + context = context + ) + } + composable(route = Screen.OCRBarcodeResults.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.OCRBarcodeResults) + OCRBarcodeResultScreen( + viewModel, + navController = navController, + innerPadding, + context = context + ) + } + composable(route = Screen.BarcodeMapResults.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.BarcodeMapResults) + BarcodeMapResultScreen( + viewModel, + navController = navController, + innerPadding, + context = context + ) + } + composable(route = Screen.CustomerInformation.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.CustomerInformation) + CustomerInformationScreen( + viewModel = viewModel, + navController = navController, + innerPadding = innerPadding + ) + } + composable(route = Screen.BarcodeScanPicking.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.BarcodeScanPicking) + BarcodeScanPickingScreen( + viewModel = viewModel, + navController = navController, + innerPadding = innerPadding + ) + } + composable(route = Screen.BarcodeMapPicking.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.BarcodeMapPicking) + BarcodeMapPickingScreen( + viewModel = viewModel, + navController = navController, + context = context, + activityInnerPadding = activityInnerPadding, + activityLifecycle = activityLifecycle + ) + } + composable(route = Screen.SingleResult.route + "?text={text}&bbox={bbox}&isBarcode={isBarcode}") { backStackEntry -> + viewModel.updateActiveScreenData(activeScreen = Screen.SingleResult) + val text = backStackEntry.arguments?.getString("text") ?: "" + val bboxStr = backStackEntry.arguments?.getString("bbox") ?: "" + val isBarcodeStr = backStackEntry.arguments?.getString("isBarcode") ?: "false" + val bboxParts = bboxStr.split(",") + val boundingBox = if (bboxParts.size == 4) { + android.graphics.Rect( + bboxParts[0].toIntOrNull() ?: 0, + bboxParts[1].toIntOrNull() ?: 0, + bboxParts[2].toIntOrNull() ?: 0, + bboxParts[3].toIntOrNull() ?: 0 + ) + } else { + android.graphics.Rect() + } + val isBarcode = isBarcodeStr == "true" + val resultRowData = ResultRowData( + text = text, + boundingBox = boundingBox, + isBarcode = isBarcode + ) + SingleResultScreen( + viewModel, + navController = navController, + innerPadding, + context = context, + resultRowData = resultRowData + ) + } + + // filter related Navigation + composable(route = Screen.OCRFindFilterHome.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.OCRFindFilterHome) + viewModel.updateSelectedFilterType(filterType = FilterType.OCR_FILTER) + OCRFindFilterHomeScreen(viewModel, navController = navController, innerPadding) + } + composable(route = Screen.BarcodeFindFilterHome.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.BarcodeFindFilterHome) + viewModel.updateSelectedFilterType(filterType = FilterType.BARCODE_FILTER) + BarcodeFindFilterHomeScreen(viewModel, navController = navController, innerPadding) + } + composable(route = Screen.RegexFilter.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.RegexFilter) + RegexFilterScreen(viewModel, navController = navController, innerPadding) + } + composable(route = Screen.CharacterTypeFilter.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.CharacterTypeFilter) + CharacterTypeFilterScreen(viewModel, navController = navController, innerPadding) + } + composable(route = Screen.CharacterMatchFilter.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.CharacterMatchFilter) + CharacterMatchFilterScreen(viewModel, navController = navController, innerPadding) + } + composable(route = Screen.StringLengthFilter.route) { + viewModel.updateActiveScreenData(activeScreen = Screen.StringLengthFilter) + StringLengthFilterScreen(viewModel, navController = navController, innerPadding) + } + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultCapturedScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultCapturedScreen.kt new file mode 100644 index 0000000..4756254 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultCapturedScreen.kt @@ -0,0 +1,246 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.ui.view + +import android.content.Context +import android.util.Log +import android.view.WindowManager +import android.view.WindowMetrics +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat.getSystemService +import androidx.navigation.NavController +import coil.compose.rememberAsyncImagePainter +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.model.FileUtils.Companion.saveOcrBarcodeCaptureSessionDataToPrefs +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import kotlin.math.min + +/** + * OCRBarcodeResultCapturedScreen is a Composable function that displays the captured image along with + * the OCR and Barcode results as overlays. It handles the back button press to navigate back to the + * previous screen and saves the capture session data to preferences when new results are available. + * The screen also calculates the necessary scaling and padding to properly display the captured image + * and overlays on different device resolutions. + */ +private const val TAG = "OCRBarcodeResultCapturedScreen" + +@Composable +fun OCRBarcodeResultCapturedScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + activityInnerPadding: PaddingValues, + context: Context +) { + val uiState = viewModel.uiState.collectAsState().value + + LaunchedEffect(key1 = uiState.ocrResults.size, key2 = uiState.barcodeResults.size) { + if ((uiState.ocrResults.size > 0) || (uiState.barcodeResults.size > 0)) { + viewModel.updateOcrBarcodeCaptureSessionIndex(uiState.ocrBarcodeCaptureSessionCount) + kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { + saveOcrBarcodeCaptureSessionDataToPrefs( + context, + uiState.ocrBarcodeCaptureSessionCount.toString(), + uiState + ) + } + Log.d(TAG, "Saved Information") + } + } + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + viewModel.updateAppBarTitle("") + val capturedBitmap = uiState.captureBitmap + if (capturedBitmap == null) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator() + } + } else { + // GET DEVICE RESOLUTION: + val displayMetrics = LocalContext.current.resources.displayMetrics + val displayMetricsDensity = displayMetrics.density + + val windowManager = getSystemService(context, WindowManager::class.java) + val windowMetrics: WindowMetrics = windowManager.currentWindowMetrics + + val displayTotalWidthInPx = windowMetrics.bounds.width() + val displayTotalHeightInPx = windowMetrics.bounds.height() + + // TOP STATUS BAR + val displayStatusBarPaddingValues = WindowInsets.statusBars.asPaddingValues() + val displayStatusBarHeightInDp = displayStatusBarPaddingValues.calculateTopPadding() + val displayStatusBarHeightInPx = displayStatusBarHeightInDp.value * displayMetricsDensity + + // BOTTOM NAVIGATION BAR + val displayNavigationBarPaddingValues = WindowInsets.navigationBars.asPaddingValues() + val displayNavigationBarHeightInDp = + displayNavigationBarPaddingValues.calculateBottomPadding() + val displayNavigationBarHeightInPx = + displayNavigationBarHeightInDp.value * displayMetricsDensity + + val availableHeightInPx = + displayTotalHeightInPx.toFloat() - displayStatusBarHeightInPx - displayNavigationBarHeightInPx + + // The following computed values are used for drawing Bbox overlay on the preview + val scaler = min( + displayTotalWidthInPx.toFloat() / capturedBitmap.width.toFloat(), + availableHeightInPx / capturedBitmap.height.toFloat() + ) + val scaledWidth = scaler * capturedBitmap.width.toFloat() + val scaledHeight = scaler * capturedBitmap.height.toFloat() + val gapX = (displayTotalWidthInPx - scaledWidth) / 2f + val gapY = (availableHeightInPx - scaledHeight) / 2f + + + Box( // Bottom layer + modifier = Modifier + .fillMaxSize() + .padding( + top = displayStatusBarHeightInDp, + bottom = displayNavigationBarHeightInDp + ) + .background(color = Color.Black) + ) { + + // CAPTURED IMAGE + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Image( + painter = rememberAsyncImagePainter(capturedBitmap), + contentDescription = "Captured Image", + contentScale = ContentScale.Fit + ) + if ((uiState.allBarcodeOCRCaptureFilter == 0 || uiState.allBarcodeOCRCaptureFilter == 2)) { + // Draw OCR results + DrawOCRResultWithTextSizeScaling( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity, + displayTotalHeightInPx = displayTotalHeightInPx, + displayTotalWidthInPx = displayTotalWidthInPx + ) + } + if ((uiState.allBarcodeOCRCaptureFilter == 0 || uiState.allBarcodeOCRCaptureFilter == 1)) { + // Draw Barcode results + DrawBarcodeResultOnCanvas( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + } + // Place RoundIconButton at bottom end with required padding + RoundIconButton( + R.drawable.ic_next, + onClick = { navController.navigate(route = Screen.OCRBarcodeResults.route) }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(bottom = 80.dp, end = 12.dp) + ) + } + } + } +} + +@Composable +private fun DrawBarcodeResultOnCanvas( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float +) { + Canvas( + modifier = Modifier + .fillMaxSize() + ) { + uiState.barcodeResults.forEach { barcodeData -> + barcodeData?.let { + + val bBoxTop = barcodeData.boundingBox.top.toFloat() + val bBoxLeft = barcodeData.boundingBox.left.toFloat() + val bBoxBottom = barcodeData.boundingBox.bottom.toFloat() + val bBoxRight = barcodeData.boundingBox.right.toFloat() + + val scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX + val scaledBBoxTopInPx = (scaler * bBoxTop) + gapY + val scaledBBoxRightInPx = (scaler * bBoxRight) + gapX + val scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + + // Define the size and position of the rectangle + val rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + val rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + + drawRect( + color = Color.Green, + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (1f * displayMetricsDensity)) + ) + + if (barcodeData.text != null && barcodeData.text != "") { + + val barcodeRectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + val paint = android.graphics.Paint().apply { + color = android.graphics.Color.WHITE + textSize = 30f + } + + val barcodeTextOffset = Offset( + scaledBBoxLeftInPx, + scaledBBoxTopInPx + (barcodeRectangleHeight) / 2 + ) + drawContext.canvas.nativeCanvas.drawText( + barcodeData.text, + barcodeTextOffset.x, + barcodeTextOffset.y, + paint + ) + } + } + } + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultScreen.kt new file mode 100644 index 0000000..a549a8a --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultScreen.kt @@ -0,0 +1,272 @@ +package com.zebra.aidatacapturedemo.ui.view + +import android.content.Context +import android.graphics.Rect +import android.util.Log +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.ResultData +import com.zebra.aidatacapturedemo.model.FileUtils.Companion.loadOcrBarcodeCaptureSessionDataFromPrefs +import com.zebra.aidatacapturedemo.ui.view.Variables.mainPrimary +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +/** + * This file defines the OCRBarcodeResultScreen composable function, which displays the results of + * OCR and barcode scanning sessions. It includes a header showing the current session index, + * a list of results with their bounding boxes, and navigation buttons to switch between sessions + * or return to the scanning screen. + * The results are loaded from the ViewModel's state or from shared preferences + * when navigating between sessions. + */ +private const val TAG = "OCRBarcodeResultCapturedScreen" + +data class ResultRowData(val text: String, val boundingBox: Rect, val isBarcode: Boolean) + +@Composable +fun OCRBarcodeResultScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavHostController, + innerPadding: PaddingValues, + context: Context +) { + val uiState = viewModel.uiState.collectAsState().value + val resultList = remember { mutableStateListOf() } + val updateResults = remember { mutableStateOf(false) } + val updateResultsTrigger = remember { mutableStateOf(0) } + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + viewModel.updateAppBarTitle(stringResource(R.string.results)) + + LaunchedEffect(updateResultsTrigger.value) { + if(uiState.ocrBarcodeCaptureSessionCount == uiState.ocrBarcodeCaptureSessionIndex){ + val ocrList = uiState.ocrResults.isNullOrEmpty().let { + uiState.ocrResults.filter { it.text.isNotEmpty() }.map { + ResultRowData(it.text, it.boundingBox, isBarcode = false) + } + } + val barcodeList = uiState.barcodeResults.isNullOrEmpty().let { + uiState.barcodeResults.filter { it.text.isNotEmpty() }.map { + ResultRowData(it.text, it.boundingBox, isBarcode = true) + } + } + resultList.clear() + resultList += ocrList + barcodeList + } else { + if (updateResults.value == true) { + val loadedResults = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { + loadSessionResults(context, uiState) + } + resultList.clear() + resultList += loadedResults + Log.d(TAG, "loadSessionResults = ${resultList.size}") + updateResults.value = false + } + } + } + Column(modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + bottom = innerPadding.calculateBottomPadding() + )) { + + Header(uiState.ocrBarcodeCaptureSessionIndex) + + LazyColumn(contentPadding = PaddingValues(vertical = 8.dp), + modifier = Modifier + .weight(1f) + .background(color = Variables.colorsSurfaceCool) + .fillMaxWidth()) { + items(resultList) { item -> + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .background(color = Variables.surfaceDefault) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = 32.dp, top = 5.dp, end = 16.dp, bottom = 5.dp) + ) { + Text( + text = item.text, + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically) + .padding(horizontal = 16.dp, vertical = 8.dp) + ) + Image( + painter = painterResource(id = R.drawable.ic_location), + contentDescription = item.text, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(horizontal = 16.dp, vertical = 8.dp) + .clickable { + val boundingBoxStr = + "${item.boundingBox.left},${item.boundingBox.top},${item.boundingBox.right},${item.boundingBox.bottom}" + navController.navigate( + "${Screen.SingleResult.route}?text=${item.text}&bbox=$boundingBoxStr&isBarcode=${item.isBarcode}" + ) + } + ) + } + } + Spacer(Modifier.height(1.dp)) + } + } + Bottom(viewModel, navController, uiState, updateResultsChanged = {updateResults.value = it}, incrementResultsTrigger = {updateResultsTrigger.value++}) + } +} + +@Composable +fun Header(session : Int){ + Row( + horizontalArrangement = Arrangement.spacedBy(107.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .width(349.dp) + .height(36.dp) + .background(color = Variables.colorsSurfaceCool) + .padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp) + ) { + Text( + text = stringResource(R.string.results_session) + session.toString(), + style = TextStyle( + fontSize = Variables.TypefaceFontSize16, + lineHeight = Variables.TypefaceLineHeight20, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.colorsMainSubtle, + letterSpacing = Variables.TypefaceLetterSpacingTitle, + ), + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically) + ) + } +} + +@Composable +fun Bottom(viewModel : AIDataCaptureDemoViewModel, navController: NavHostController, uiState: AIDataCaptureDemoUiState, + updateResultsChanged: (Boolean) -> Unit, + incrementResultsTrigger: () -> Unit) { + + Row(modifier = Modifier + .fillMaxWidth() + .padding(start = 8.dp, top = 8.dp, end = 8.dp, bottom = 8.dp) + .border(1.dp, Variables.mainInverse), + verticalAlignment = Alignment.CenterVertically + ) { + + Row(modifier = Modifier + .padding(start = 16.dp, top = 8.dp, end = 8.dp, bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically + ) { + + RoundedIconButton(R.drawable.ic_previoussession, onClick = { + val index = uiState.ocrBarcodeCaptureSessionIndex - 1 + when { + index < 1 -> 1 + index > uiState.ocrBarcodeCaptureSessionCount -> uiState.ocrBarcodeCaptureSessionCount + else -> { + viewModel.updateOcrBarcodeCaptureSessionIndex(index) + updateResultsChanged(true) + incrementResultsTrigger() + } + } + }) + RoundedIconButton(R.drawable.ic_nextsession, onClick = { + val index = uiState.ocrBarcodeCaptureSessionIndex + 1 + when { + index < 1 -> 1 + index > uiState.ocrBarcodeCaptureSessionCount -> uiState.ocrBarcodeCaptureSessionCount + else -> { + viewModel.updateOcrBarcodeCaptureSessionIndex(index) + updateResultsChanged(true) + incrementResultsTrigger() + } + } + }) + Spacer(modifier = Modifier.weight(1f)) + ButtonWithIconOption( + ButtonData( + R.string.scan, + mainPrimary, + 1.0F, + true, + onButtonClick = { + navController.navigate(route = Screen.Preview.route) { + popUpTo("preview_screen") { + inclusive = true + } + launchSingleTop = true // Prevents multiple copies of the same destination at the top of the stack + } + } + ), + R.drawable.ic_scan + ) + } + } +} +private fun loadSessionResults(context: Context, uiState: AIDataCaptureDemoUiState): List { + val sessionJson = loadOcrBarcodeCaptureSessionDataFromPrefs(context, uiState.ocrBarcodeCaptureSessionIndex.toString()) + val ocrList = if (!sessionJson?.ocrResults.isNullOrEmpty()) { + sessionJson.ocrResults.filter { it.text.isNotEmpty() }.map { + ResultRowData(it.text, it.boundingBox, isBarcode = false) + } + } else { + emptyList() + } + val barcodeList = if (!sessionJson?.barcodeResults.isNullOrEmpty()) { + sessionJson.barcodeResults.filter { it.text.isNotEmpty() }.map { + ResultRowData(it.text, it.boundingBox, isBarcode = true) + } + } else { + emptyList() + } + return ocrList + barcodeList +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRModelSettings.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRModelSettings.kt new file mode 100644 index 0000000..164eece --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRModelSettings.kt @@ -0,0 +1,441 @@ +package com.zebra.aidatacapturedemo.ui.view + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +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.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.ui.view.Variables.blackText +import com.zebra.aidatacapturedemo.ui.view.Variables.warningBorder +import com.zebra.aidatacapturedemo.ui.view.Variables.warningColor +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +/** + * AdvancedOCRSettingsScreen is a Composable function that displays the advanced settings for the + * OCR model in the AI Data Capture Demo app. It includes options for detection parameters, + * recognition parameters, and grouping settings. The screen also provides a warning message and + * a link to TechDocs for more information. The settings are displayed in an expandable list format, + * allowing users to easily navigate and modify the OCR settings as needed. + * + * @param viewModel The ViewModel instance that holds the UI state and handles user interactions for the AI Data Capture Demo. + * @param navController The NavController used for navigation between screens in the app. + * @param innerPadding The padding values to be applied to the content of the screen, typically provided by Scaffold. + * @param context The Context of the current state of the application, used for actions such as opening URLs. + */ +@Composable +fun AdvancedOCRSettingsScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + innerPadding: PaddingValues, + context: Context +) { + val uiState = viewModel.uiState.collectAsState().value + // Intercept back presses on this screen + val demo = uiState.usecaseSelected + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + viewModel.updateAppBarTitle(stringResource(R.string.advanced_settings)) + val settingsItemsList = ExpandableSettingsItemsList() + settingsItemsList.AddOCRSettings() + + Column( + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .background(color = Variables.surfaceDefault) + ) { + Column( + modifier = Modifier.padding(top = 12.dp, start = 12.dp, end = 12.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.Start), + verticalAlignment = Alignment.Top, + modifier = Modifier + .border(width = 1.dp, warningBorder, shape = RoundedCornerShape(size = 4.dp)) + .padding(0.5.dp) + .fillMaxWidth() + .wrapContentHeight() + .background(color = warningColor, shape = RoundedCornerShape(size = 4.dp)) + .padding(12.dp) + ) { + Image( + painter = painterResource(id = R.drawable.warning_icon), + contentDescription = "image description", + contentScale = ContentScale.None, + modifier = Modifier + .padding(0.75.dp) + .width(18.dp) + .height(18.dp) + ) + Text( + text = stringResource(R.string.instruction_3), + style = TextStyle( + fontSize = 13.sp, + lineHeight = 18.93.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = blackText, + letterSpacing = 0.24.sp, + ) + ) + } + } + + Row( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp) + ) { + Text( + text = "Visit Techdocs for information on the advanced settings >", + style = TextStyle( + fontSize = 14.sp, + lineHeight = 18.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainPrimary, + ), + modifier = Modifier.clickable { + openTechDocsUrl(context = context) + } + ) + } + + val items = remember { + List(settingsItemsList.itemsTitle.size) { index -> + ExpandableSettingsItem(settingsItemsList.itemsTitle[index].title) + } + } + val expandedStates = + remember { mutableStateListOf(*BooleanArray(items.size) { false }.toTypedArray()) } + val listState = rememberLazyListState() + + LazyColumn( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top), + horizontalAlignment = Alignment.Start, + contentPadding = PaddingValues(top = 16.dp, bottom = 32.dp), + state = listState + ) { + itemsIndexed(items, key = { index, _ -> index }) { index, item -> + ExpandableSettingsListItem( + item = item, + index = index, + isExpanded = expandedStates[index], + onExpandedChange = { + for (i in items.indices) { + expandedStates[i] = false + } + expandedStates[index] = it + }, + viewModel, navController + ) + } + } + } +} + +private fun openTechDocsUrl(context: Context) { + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://techdocs.zebra.com/ai-datacapture/latest/textocr/") + ) + context.startActivity(intent) +} + +@Composable +fun ExpandableSettingsItemsList.AddOCRSettings() { + itemsTitle.add(ExpandableSettingsItem(stringResource(R.string.detection_parameters))) + itemsTitle.add(ExpandableSettingsItem(stringResource(R.string.recognition_parameters))) + itemsTitle.add(ExpandableSettingsItem(stringResource((R.string.grouping)))) +} + +@Composable +fun AddOCRDetectionOptions(viewModel: AIDataCaptureDemoViewModel) { + val currentUIState = viewModel.uiState.collectAsState().value + Column( + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top), + horizontalAlignment = Alignment.Start, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + TextInputOption( + TextInputData( + R.string.heatmap_threshold, + currentUIState.textOCRSettings.advancedOCRSetting.heatmapThreshold.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }) + ) + TextInputOption( + TextInputData( + R.string.box_threshold, + currentUIState.textOCRSettings.advancedOCRSetting.boxThreshold.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }) + ) + TextInputOption( + TextInputData( + R.string.min_box_area, + currentUIState.textOCRSettings.advancedOCRSetting.minBoxArea.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }) + ) + TextInputOption( + TextInputData( + R.string.min_box_size, + currentUIState.textOCRSettings.advancedOCRSetting.minBoxSize.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }) + ) + TextInputOption( + TextInputData( + R.string.unclip_ratio, + currentUIState.textOCRSettings.advancedOCRSetting.unclipRatio.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }) + ) + TextInputOption( + TextInputData( + R.string.min_ratio_for_rotation, + currentUIState.textOCRSettings.advancedOCRSetting.minRatioForRotation.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }) + ) + } +} + +@Composable +fun AddOCRRecognitionOptions(viewModel: AIDataCaptureDemoViewModel) { + var tiling by remember { mutableStateOf(false) } + val currentUIState = viewModel.uiState.collectAsState().value + Column( + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top), + horizontalAlignment = Alignment.Start, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + TextInputOption( + TextInputData( + R.string.max_word_combinations, + currentUIState.textOCRSettings.advancedOCRSetting.maxWordCombinations.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }) + ) + TextInputOption( + TextInputData( + R.string.topk_ignore_cutoff, + currentUIState.textOCRSettings.advancedOCRSetting.topkIgnoreCutoff.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }) + ) + TextInputOption( + TextInputData( + R.string.total_probability_threshold, + currentUIState.textOCRSettings.advancedOCRSetting.totalProbabilityThreshold.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }) + ) + //} + HorizontalDivider( + modifier = Modifier + .fillMaxWidth().padding(horizontal = 5.dp), + thickness = 2.dp + ) + SwitchOption( + currentUIState.textOCRSettings.advancedOCRSetting.enableTiling, + SwitchOptionData(R.string.enable_tiling, onItemSelected = { title, enabled -> + tiling = enabled + viewModel.updateOCRSwitchOptions(title, enabled) + }) + ) + AddOCRTilingOptions(viewModel, tiling) + } +} + +@Composable +fun AddOCRTilingOptions(viewModel: AIDataCaptureDemoViewModel, enabled: Boolean) { + val currentUIState = viewModel.uiState.collectAsState().value + Column( + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top), + horizontalAlignment = Alignment.Start, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + TextInputOption( + TextInputData( + R.string.top_correlation_threshold, + currentUIState.textOCRSettings.advancedOCRSetting.topCorrelationThreshold.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }), enabled + ) + TextInputOption( + TextInputData( + R.string.merge_points_cutoff, + currentUIState.textOCRSettings.advancedOCRSetting.mergePointsCutoff.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }), enabled + ) + TextInputOption( + TextInputData( + R.string.split_margin_factor, + currentUIState.textOCRSettings.advancedOCRSetting.splitMarginFactor.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }), enabled + ) + TextInputOption( + TextInputData( + R.string.aspect_ratio_lower_threshold, + currentUIState.textOCRSettings.advancedOCRSetting.aspectRatioLowerThreshold.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }), enabled + ) + TextInputOption( + TextInputData( + R.string.aspect_ratio_upper_threshold, + currentUIState.textOCRSettings.advancedOCRSetting.aspectRatioUpperThreshold.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }), enabled + ) + TextInputOption( + TextInputData( + R.string.topK_merged_predictions, + currentUIState.textOCRSettings.advancedOCRSetting.topKMergedPredictions.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }), enabled + ) + } +} + +@Composable +fun AddEnableOCRGroupingOptions(viewModel: AIDataCaptureDemoViewModel) { + val currentUIState = viewModel.uiState.collectAsState().value + var grouping by remember { mutableStateOf(false) } + Column( + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top), + horizontalAlignment = Alignment.Start, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + SwitchOption( + currentUIState.textOCRSettings.advancedOCRSetting.enableGrouping, + SwitchOptionData(R.string.enable_grouping, onItemSelected = { title, enabled -> + grouping = enabled + viewModel.updateOCRSwitchOptions(title, enabled) + }) + ) + AddOCRGroupingOptions(viewModel, grouping) + } +} + +@Composable +fun AddOCRGroupingOptions(viewModel: AIDataCaptureDemoViewModel, enabled: Boolean) { + val currentUIState = viewModel.uiState.collectAsState().value + Column( + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top), + horizontalAlignment = Alignment.Start, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + TextInputOption( + TextInputData( + R.string.width_distance_ratio, + currentUIState.textOCRSettings.advancedOCRSetting.widthDistanceRatio.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }), enabled + ) + TextInputOption( + TextInputData( + R.string.height_distance_ratio, + currentUIState.textOCRSettings.advancedOCRSetting.heightDistanceRatio.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }), enabled + ) + TextInputOption( + TextInputData( + R.string.center_distance_ratio, + currentUIState.textOCRSettings.advancedOCRSetting.centerDistanceRatio.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }), enabled + ) + TextInputOption( + TextInputData( + R.string.paragraph_height_distance, + currentUIState.textOCRSettings.advancedOCRSetting.paragraphHeightDistance.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }), enabled + ) + TextInputOption( + TextInputData( + R.string.paragraph_height_ratio_threshold, + currentUIState.textOCRSettings.advancedOCRSetting.paragraphHeightRatioThreshold.toString(), + onItemSelected = { title, value -> + viewModel.updateOCRTextFieldValues(title, value) + }), enabled + ) + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/ProductsResultCapturedScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/ProductsResultCapturedScreen.kt new file mode 100644 index 0000000..c7d89c0 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/ProductsResultCapturedScreen.kt @@ -0,0 +1,666 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.ui.view + +import android.content.Context +import android.view.WindowManager +import android.view.WindowMetrics +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.TextSelectionColors +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat.getSystemService +import androidx.navigation.NavController +import coil.compose.rememberAsyncImagePainter +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.ProductData +import com.zebra.aidatacapturedemo.model.FileUtils +import com.zebra.aidatacapturedemo.ui.view.Variables.borderPrimaryMain +import com.zebra.aidatacapturedemo.ui.view.Variables.mainPrimary +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import kotlinx.coroutines.CoroutineScope +import kotlin.math.min + +private const val TAG = "ProductsResultCapturedScreen" + +/** + * ProductsResultCapturedScreen composable function to display the captured high resolution + * image with bounding boxes and product SKU's + * The user can tap on the product bounding boxes that brings up a dialog box. + * The dialog box displays cropped product image displayed and an edit box wherein the user can + * input SKU manually, or scan a barcode by pressing yellow scan button that then invokes + * Datawedge Profile 0 (in enabled) to scan the barcode. + * User can then press confirm button to associate the SKU to the product image and bounding box. + * When the user presses save product database button, the products.db is saved in the Downloads + * folder and a timestamped folder is created in Pictures Folder, + * within which Product SKU folder is created and the product image crops are saved that folder. + */ +@Composable +fun ProductsResultCapturedScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + activityInnerPadding: PaddingValues, + context: Context +) { + val uiState = viewModel.uiState.collectAsState().value + val productResultsList = uiState.productResults + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + + val capturedBitmap = uiState.captureBitmap + if (capturedBitmap == null) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator() + } + } else { + + var showInfo = remember { mutableStateOf(true) } + + var isProductEnrollmentProgressBarVisible by remember { mutableStateOf(false) } + + // GET DEVICE RESOLUTION: + val displayMetrics = LocalContext.current.resources.displayMetrics + val displayMetricsDensity = displayMetrics.density + + val windowManager = getSystemService(context, WindowManager::class.java) + val windowMetrics: WindowMetrics = windowManager.currentWindowMetrics + + val displayTotalWidthInPx = windowMetrics.bounds.width() + val displayTotalHeightInPx = windowMetrics.bounds.height() + + // TOP STATUS BAR + val displayStatusBarPaddingValues = WindowInsets.statusBars.asPaddingValues() + val displayStatusBarHeightInDp = displayStatusBarPaddingValues.calculateTopPadding() + val displayStatusBarHeightInPx = displayStatusBarHeightInDp.value * displayMetricsDensity + + // BOTTOM NAVIGATION BAR + val displayNavigationBarPaddingValues = WindowInsets.navigationBars.asPaddingValues() + val displayNavigationBarHeightInDp = + displayNavigationBarPaddingValues.calculateBottomPadding() + val displayNavigationBarHeightInPx = + displayNavigationBarHeightInDp.value * displayMetricsDensity + + val availableHeightInPx = + displayTotalHeightInPx.toFloat() - displayStatusBarHeightInPx - displayNavigationBarHeightInPx + + // The following computed values are used for drawing Bbox overlay on the preview + val scaler = min( + displayTotalWidthInPx.toFloat() / capturedBitmap.width.toFloat(), + availableHeightInPx / capturedBitmap.height.toFloat() + ) + val scaledWidth = scaler * capturedBitmap.width.toFloat() + val scaledHeight = scaler * capturedBitmap.height.toFloat() + val gapX = (displayTotalWidthInPx - scaledWidth) / 2f + val gapY = (availableHeightInPx - scaledHeight) / 2f + + + Box( // Bottom layer + modifier = Modifier + .fillMaxSize() + .padding( + top = displayStatusBarHeightInDp, + bottom = displayNavigationBarHeightInDp + ) + .background(color = Color.Black) + ) { + + // CAPTURED IMAGE + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Image( + painter = rememberAsyncImagePainter(capturedBitmap), + contentDescription = "Captured Image", + contentScale = ContentScale.Fit + ) + } + + // draw Shelf Label, Peg Label & Shelf Row only + DrawRetailShelfLabelsAndRowsUsingBox( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + + // draw products (with Recognition) + DrawRetailShelfProductsUsingBox( + productResultsList = productResultsList, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + + if (showInfo.value) { + val topInfoInstruction = if (isProductEnrollmentProgressBarVisible) { + stringResource(R.string.instruction_enrolling_into_db) + } else if (productResultsList.isEmpty()) { + stringResource(R.string.instruction_5) + } else { + stringResource(R.string.instruction_2) + } + + val startIcon = if (productResultsList.isEmpty()) { + R.drawable.warning_icon + } else { + R.drawable.icon_add + } + + HandleTopInfo( + startIcon, + topInfoInstruction, + showInfo + ) + } + } + + if (productResultsList.isNotEmpty()) { + val coroutineScope = rememberCoroutineScope() + + DrawEnrollProductsIcon( + isProductEnrollmentProgressBarVisible = isProductEnrollmentProgressBarVisible, + isProductEnrollmentProgressBarVisibleOnChange = { + isProductEnrollmentProgressBarVisible = it + }, + uiState = uiState, + viewModel = viewModel, + coroutineScope = coroutineScope, + productResults = productResultsList, + navController = navController, + activityInnerPadding = activityInnerPadding + ) + } + } +} + +@Composable +private fun DrawRetailShelfLabelsAndRowsUsingBox( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float, +) { + uiState.bboxes.filter { it?.cls != 1 }.forEach { bBox -> + bBox?.let { + + val bBoxTop = bBox.ymin + val bBoxLeft = bBox.xmin + val bBoxBottom = bBox.ymax + val bBoxRight = bBox.xmax + + val scaledBBoxLeftInDp = (((scaler * bBoxLeft) + gapX) / displayMetricsDensity).dp + val scaledBBoxTopInDp = (((scaler * bBoxTop) + gapY) / displayMetricsDensity).dp + val scaledBBoxRightInDp = (((scaler * bBoxRight) + gapX) / displayMetricsDensity).dp + val scaledBBoxBottomInDp = (((scaler * bBoxBottom) + gapY) / displayMetricsDensity).dp + + val rectangleWidth = scaledBBoxRightInDp - scaledBBoxLeftInDp + val rectangleHeight = scaledBBoxBottomInDp - scaledBBoxTopInDp +// val topLeftOffset = Offset(scaledBBoxLeftInDp, scaledBBoxTopInDp) + + when (bBox.cls) { + 2, 3 -> { //Shelf Labels, Peg Labels + Box( + modifier = Modifier + .padding( + start = scaledBBoxLeftInDp, + top = scaledBBoxTopInDp + ) + .border( + BorderStroke(width = 1.dp, color = Color.Blue) + ) + .width(width = rectangleWidth) + .height(height = rectangleHeight) + ) + } + + 4 -> { //Shelf Row + Box( + modifier = Modifier + .padding( + start = scaledBBoxLeftInDp, + top = scaledBBoxTopInDp + ) + .border( + BorderStroke(width = 1.dp, color = Color.Red) + ) + .width(width = rectangleWidth) + .height(height = rectangleHeight) + ) + } + + else -> { // unknown + Box( + modifier = Modifier + .padding( + start = scaledBBoxLeftInDp, + top = scaledBBoxTopInDp + ) + .border( + BorderStroke(width = 1.dp, color = Color.Magenta) + ) + .width(width = rectangleWidth) + .height(height = rectangleHeight) + ) + } + } + } + } +} + +@Composable +private fun DrawRetailShelfProductsUsingBox( + productResultsList: MutableList, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float +) { + productResultsList.forEach { productData -> + val bBoxTop = productData.bBox.ymin + val bBoxLeft = productData.bBox.xmin + val bBoxBottom = productData.bBox.ymax + val bBoxRight = productData.bBox.xmax + + val scaledBBoxLeftInDp = (((scaler * bBoxLeft) + gapX) / displayMetricsDensity).dp + val scaledBBoxTopInDp = (((scaler * bBoxTop) + gapY) / displayMetricsDensity).dp + val scaledBBoxRightInDp = (((scaler * bBoxRight) + gapX) / displayMetricsDensity).dp + val scaledBBoxBottomInDp = (((scaler * bBoxBottom) + gapY) / displayMetricsDensity).dp + + val rectangleWidth = scaledBBoxRightInDp - scaledBBoxLeftInDp + val rectangleHeight = scaledBBoxBottomInDp - scaledBBoxTopInDp + + val productShowDialog = remember { mutableStateOf(false) } + val productSKUChanged = remember { mutableStateOf(false) } + var productSKU = rememberSaveable { mutableStateOf(productData.text) } + + if (productShowDialog.value) { + ProductAlertDialog( + productShowDialog = productShowDialog, + productImage = productData.crop.asImageBitmap(), + productSKU = productSKU, + productSKUChanged = productSKUChanged + ) + } + + // After effect of ProductAlertDialog opened and Closed + if (productSKUChanged.value) { + productData.text = productSKU.value + productSKUChanged.value = false + } + + if (productData.text != null && productData.text != "") { // Product is Recognized with Higher confidence + Box( + modifier = Modifier + .padding( + start = scaledBBoxLeftInDp, + top = scaledBBoxTopInDp + ) + .border( + BorderStroke(width = 1.dp, color = Color.Green) + ) + .background(color = Color(0xAA004830).copy(alpha = 0.5F)) + .width(width = rectangleWidth) + .height(height = rectangleHeight) + .clickable { + productShowDialog.value = true + } + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.CenterStart + ) { + Text( + text = productData.text, + color = Color.White, + fontSize = (30f / displayMetricsDensity).sp + ) + } + } + } else { // Product not Recognized + Box( + modifier = Modifier + .padding( + start = scaledBBoxLeftInDp, + top = scaledBBoxTopInDp + ) + .border( + BorderStroke(width = 1.dp, color = Color.Green) + ) + .width(width = rectangleWidth) + .height(height = rectangleHeight) + .clickable { + productShowDialog.value = true + } + ) + } + } +} + +@Composable +fun DrawEnrollProductsIcon( + isProductEnrollmentProgressBarVisible: Boolean, + isProductEnrollmentProgressBarVisibleOnChange: (Boolean) -> Unit, + uiState: AIDataCaptureDemoUiState, + coroutineScope: CoroutineScope, + productResults: MutableList, + navController: NavController, + viewModel: AIDataCaptureDemoViewModel, + activityInnerPadding: PaddingValues +) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(bottom = activityInnerPadding.calculateBottomPadding()) + .background(Color.Black.copy(alpha = 0.4f)), + ) { + Row( + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + top = 12.dp, + bottom = 12.dp + ), + horizontalArrangement = Arrangement.Center + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .border( + width = 1.dp, + borderPrimaryMain, + shape = RoundedCornerShape(size = 4.dp) + ) + .padding( + start = Variables.spacingLarge, + top = Variables.spacingMedium, + end = Variables.spacingLarge, + bottom = Variables.spacingMedium + ) + .fillMaxWidth() + .wrapContentHeight() + .clickable { + saveProductDataList(viewModel, productResults) + viewModel.enrollProductIndex() + isProductEnrollmentProgressBarVisibleOnChange(true) + } + ) { + Text( + text = stringResource(R.string.save_active_database), + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_regular)), + fontWeight = FontWeight(500), + color = Variables.stateDefaultEnabled, + ) + ) + } + } + } + } + + if (isProductEnrollmentProgressBarVisible) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.5f)), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = Variables.borderDefault, trackColor = mainPrimary) + } + } + + if (uiState.isProductEnrollmentCompleted) { + viewModel.handleBackButton(navController) + isProductEnrollmentProgressBarVisibleOnChange(false) + } +} + + +/** + * Save the product data (SKU and Image) list to the database and save the product image crops to the Pictures folder + */ +fun saveProductDataList( + viewModel: AIDataCaptureDemoViewModel, + productResults: MutableList +) { + val timestampedFolder = FileUtils.getTimeStampedFolderName() + for (productData in productResults) { + if (productData.text.isNotEmpty()) { + FileUtils.saveBitmap( + productData.crop, + timestampedFolder + "/" + productData.text, + "productcrop" + ) + } + } + viewModel.toast("Saved product crops in ${timestampedFolder} ") +} + +/** + * ProductAlertDialog composable function to display the dialog box when the user taps on the product bounding box + * The dialog box displays cropped product image displayed and an edit box wherein the user can + * input SKU manually, or scan a barcode by pressing yellow scan button that then invokes + * Datawedge Profile 0 (in enabled) to scan the barcode. + * User can then press confirm button to associate the SKU to the product image and bounding box. + */ +@Composable +fun ProductAlertDialog( + productShowDialog: MutableState, + productImage: ImageBitmap, + productSKU: MutableState, + productSKUChanged: MutableState +) { + var savedOriginalSKU: String = productSKU.value + if (productShowDialog.value) { + val focusRequester = remember { FocusRequester() } + AlertDialog( + onDismissRequest = { + productSKUChanged.value = false + productSKU.value = savedOriginalSKU + productShowDialog.value = false + }, + title = { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, top = 20.dp, end = 20.dp) + ) { + Text( + text = stringResource(R.string.enterproductsku), + style = TextStyle( + fontSize = 20.sp, + lineHeight = 28.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Variables.mainDefault, + ) + ) + } + }, + containerColor = Variables.surfaceDefault, + text = { + Column(Modifier.fillMaxWidth()) { + Image( + painter = BitmapPainter( + productImage, + IntOffset(0, 0), + IntSize(productImage.width, productImage.height) + ), + contentDescription = "Product Crop", + contentScale = ContentScale.FillBounds, + modifier = Modifier + .size(200.dp) + .align(Alignment.CenterHorizontally) + ) + TextField( + value = productSKU.value, + placeholder = { Text(stringResource(R.string.enterproducthint)) }, + onValueChange = { productSKU.value = it }, + maxLines = 1, + textStyle = TextStyle( + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.mainDefault + ), + modifier = Modifier + .background( + color = Color(0xFFFFFFFF), + shape = RoundedCornerShape(size = 3.6.dp) + ) + .fillMaxWidth() + .padding(start = 14.4.dp, top = 8.dp, end = 14.4.dp, bottom = 8.dp) + .focusRequester(focusRequester), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + colors = TextFieldDefaults.colors( + focusedContainerColor = Variables.mainInverse, + unfocusedContainerColor = Variables.mainInverse, + cursorColor = Variables.mainPrimary, + focusedIndicatorColor = Variables.mainPrimary, + unfocusedIndicatorColor = Variables.mainPrimary, + selectionColors = TextSelectionColors( + handleColor = mainPrimary, + backgroundColor = mainPrimary + ) + ) + ) + } + }, + confirmButton = { + Button( + onClick = { + productSKUChanged.value = true + productShowDialog.value = false + }, + modifier = Modifier + .height(48.dp) + .width(121.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Variables.mainPrimary + ), + shape = RoundedCornerShape(4.dp) + ) + { + Text( + text = stringResource(R.string.apply), + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.stateDefaultEnabled, + textAlign = TextAlign.Center, + ) + ) + } + }, + dismissButton = { + Button( + onClick = { + productSKUChanged.value = false + productShowDialog.value = false + productSKU.value = savedOriginalSKU + }, + modifier = Modifier + .height(48.dp) + .width(121.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent + ) + ) { + Text( + text = stringResource(R.string.cancel), + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.mainPrimary, + textAlign = TextAlign.Center, + ) + ) + } + }, + ) + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/RetailModelSettings.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/RetailModelSettings.kt new file mode 100644 index 0000000..fdc3ada --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/RetailModelSettings.kt @@ -0,0 +1,185 @@ +package com.zebra.aidatacapturedemo.ui.view + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.UsecaseState +import com.zebra.aidatacapturedemo.model.FileUtils +import com.zebra.aidatacapturedemo.ui.view.Variables.mainPrimary +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +/** + * This file contains composable functions related to the settings for the Retail and Product + * Recognition use cases in the AI Data Capture Demo. + * It includes functions to add settings items to the expandable list, as well as specific options + * for importing/exporting databases and adjusting similarity thresholds. + */ +@Composable +fun ExpandableSettingsItemsList.AddProductEnrollmentSettings() { + itemsTitle.add(ExpandableSettingsItem(stringResource(R.string.import_database))) + itemsTitle.add(ExpandableSettingsItem(stringResource(R.string.export_database))) + itemsTitle.add(ExpandableSettingsItem(stringResource((R.string.clear_active_database)))) + itemsTitle.add(ExpandableSettingsItem(stringResource((R.string.similarity_threshold)))) +} + +@Composable +fun ExpandableSettingsItemsList.AddProductRecognitionSettings() { + itemsTitle.add(ExpandableSettingsItem(stringResource((R.string.similarity_threshold)))) +} +@Composable +fun AddImportDatabaseOptions(viewModel: AIDataCaptureDemoViewModel) { + val productLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? -> + if (uri != null) { + viewModel.loadProductIndex(uri) + } + } + + fun productLauncherFunc() { + productLauncher.launch(arrayOf("*/*")) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(color = Variables.mainInverse) + ) { + Row( + modifier = Modifier.padding(start = 12.dp, top = 16.dp, end = 12.dp, bottom = 16.dp) + ) { + Text( + text = stringResource(R.string.importdb_description), + style = TextStyle( + fontSize = 14.sp, + lineHeight = 18.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainDefault, + ) + ) + } + } + + ButtonOption(ButtonData(R.string.import_database, mainPrimary, 1.0F, true, onButtonClick = { + productLauncherFunc() + })) +} + +@Composable +fun AddExportDatabaseOptions(viewModel: AIDataCaptureDemoViewModel) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(color = Variables.mainInverse) + ) { + Row( + modifier = Modifier.padding(start = 12.dp, top = 16.dp, end = 12.dp, bottom = 16.dp) + ) { + Text( + text = stringResource(R.string.exportdb_description), + style = TextStyle( + fontSize = 14.sp, + lineHeight = 18.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainDefault, + ) + ) + } + } + + ButtonOption(ButtonData(R.string.export_database, mainPrimary, 1.0F, true, onButtonClick = { + FileUtils.saveProductDBFile() + viewModel.toast("Saved Active Database to \"Download\" folder ") + })) +} + +@Composable +fun AddClearActiveDatabaseOptions(viewModel: AIDataCaptureDemoViewModel) { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(color = Variables.mainInverse) + ) { + Row( + modifier = Modifier.padding(start = 12.dp, top = 16.dp, end = 12.dp, bottom = 16.dp) + ) { + Text( + text = stringResource(R.string.cleardb_description), + style = TextStyle( + fontSize = 14.sp, + lineHeight = 18.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainDefault, + ) + ) + } + } + + + ButtonOption( + ButtonData( + R.string.clear_active_database, + mainPrimary, + 1.0F, + true, + onButtonClick = { + viewModel.deleteProductIndex() + viewModel.toast("Cleared Active Database") + }) + ) +} + +@Composable +fun AddSimilarityThreshold(viewModel: AIDataCaptureDemoViewModel) { + val currentUIState = viewModel.uiState.collectAsState().value + val currentSimilarityThreshold = when (currentUIState.usecaseSelected) { + UsecaseState.Retail.value -> { + currentUIState.retailShelfSettings.similarityThreshold + } + UsecaseState.Product.value -> { + currentUIState.productRecognitionSettings.similarityThreshold + } + else -> { + 80f + } + } + var sliderValue = remember { mutableFloatStateOf(currentSimilarityThreshold) } + SingleValueInputSlider( + value = sliderValue.value, + onValueChange = { + sliderValue.value = it + viewModel.updateSimilarityThreshold(it) + }, + label = "Select Similarity Threshold", + ) + Spacer(modifier = Modifier.height(16.dp)) +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/SettingsMoreInfoScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/SettingsMoreInfoScreen.kt new file mode 100644 index 0000000..e64599d --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/SettingsMoreInfoScreen.kt @@ -0,0 +1,426 @@ +package com.zebra.aidatacapturedemo.ui.view + +import android.text.Html +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.BulletSpan +import android.util.TypedValue +import android.widget.Space +import android.widget.TextView +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.res.ResourcesCompat +import androidx.navigation.NavController +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.ui.view.Variables.mainDefault +import com.zebra.aidatacapturedemo.ui.view.Variables.mainInverse +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +/** + * SettingsMoreInfoScreen.kt + * + * This file contains the implementation of the SettingsMoreInfoScreen composable function, + * which displays a modal bottom sheet with detailed information and recommendations for a + * specific document. The screen includes expandable list items that show additional details and + * tips when expanded. The content is dynamically loaded from the provided Document object, + * allowing for flexible and rich information display. + */ +val itemsTitle: MutableList = mutableStateListOf() + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsMoreInfoScreen( + viewModel: AIDataCaptureDemoViewModel, + document: Document, + showSheet: Boolean +): Boolean { + itemsTitle.clear() + val sheetState = rememberModalBottomSheetState() + var showBottomSheet: MutableState = remember { mutableStateOf(showSheet) } + rememberCoroutineScope() + if (showBottomSheet.value) { + ModalBottomSheet( + onDismissRequest = { + showBottomSheet.value = false + }, + sheetState = sheetState, + shape = RoundedCornerShape(topStart = 36.dp, topEnd = 36.dp), + containerColor = Variables.surfaceDefault, + tonalElevation = 16.dp, + dragHandle = { + Box( + modifier = Modifier + .padding(top = 24.dp, bottom = 20.dp) + .width(44.dp) + .height(6.dp) + .clip(RoundedCornerShape(50)) + .background(Variables.mainDisabled) + ) + }, + modifier = Modifier.fillMaxWidth() + ) { + Box(modifier = Modifier.fillMaxSize()) { + SettingMoreInfoMain(viewModel, document) + } + } + } + return showBottomSheet.value +} + +@Composable +fun SettingMoreInfoMain(viewModel: AIDataCaptureDemoViewModel, document: Document) { + Column( + verticalArrangement = Arrangement.spacedBy(20.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .background(color = Variables.surfaceDefault, shape = RoundedCornerShape(size = 36.dp)) + .padding(top = 24.dp, bottom = 24.dp) + ) { + AnimateMoreInfoExpandableList(viewModel, document) + } +} + +data class MoreInfoExpandableItem( + val title: String, + var isExpanded: Boolean = false +) + +@Composable +fun AnimateMoreInfoExpandableList( + viewModel: AIDataCaptureDemoViewModel, + document: Document) { + val element: Element? = document.getElementById("title") + var htmlString = "" + if(element != null) { + htmlString = element.html() + itemsTitle.add(htmlString) + itemsTitle.add(stringResource(R.string.recommendation_tips)) + } + val items = remember { List(itemsTitle.size) { index -> MoreInfoExpandableItem( itemsTitle[index]) } } + val expandedStates = remember { mutableStateListOf(*BooleanArray(items.size) { false }.toTypedArray()) } + val listState = rememberLazyListState() + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .border(width = 1.dp, color = Variables.borderDefault) + .fillMaxWidth().padding(24.dp) + .height(48.dp), + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top), + horizontalAlignment = Alignment.Start, + contentPadding = PaddingValues(top = 16.dp, bottom = 32.dp), + state = listState + ) { + item { + Column( + verticalArrangement = Arrangement.spacedBy(20.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 24.dp, end = 24.dp) + ) { + val elementTip: Element? = document.getElementById("description") + var summaryHtmlString = AnnotatedString("") + if(elementTip != null) { + summaryHtmlString = AnnotatedString.fromHtml(elementTip.html()) + } + Text( + text = summaryHtmlString, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainDefault, + ) + ) + } + } + itemsIndexed(items, key = { index, _ -> index }) { index, item -> + ExpandableMoreInfoListItem( + item = item, + index = index, + isExpanded = expandedStates[index], + onExpandedChange = { + for (i in items.indices) { + expandedStates[i] = false + } + expandedStates[index] = it + }, + viewModel, document + ) + } + } +} + +@Composable +fun ExpandableMoreInfoListItem( + item: MoreInfoExpandableItem, + index: Int, + isExpanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + viewModel: AIDataCaptureDemoViewModel, + document: Document +) { + val interactionSource = remember { MutableInteractionSource() } + val rotationAngle by animateFloatAsState(targetValue = if (isExpanded) 180f else 0f) + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top), + horizontalAlignment = Alignment.Start, + modifier = Modifier + .fillMaxWidth() + .shadow(4.dp, shape = RoundedCornerShape(12.dp)) + .background(color = Variables.surfaceDefault, shape = RoundedCornerShape(12.dp)) + .clickable(interactionSource = interactionSource, indication = null) { + onExpandedChange(!isExpanded) + } + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .border(width = 1.dp, color = Variables.borderDefault) + .fillMaxWidth() + .wrapContentHeight() + .background(color = Variables.mainInverse) + .padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 12.dp) + ) { + Text( + text = item.title, + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_regular)), + fontWeight = FontWeight(500), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = ImageVector.Companion.vectorResource(id = R.drawable.down_arrow_icon), + contentDescription = if (isExpanded) "Collapse" else "Expand", + modifier = Modifier + .graphicsLayer(rotationZ = rotationAngle) + .padding(1.dp) + .width(20.dp) + .height(20.dp), + tint = Variables.mainSubtle + ) + } + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + if (item.title.equals(stringResource(R.string.recommendation_tips))) { + SettingMoreInfoRecommendationTip(viewModel, document) + } else { + SettingMoreInfoDetails(viewModel, document) + } + } + } +} + +@Composable +fun SettingMoreInfoDetails(viewModel: AIDataCaptureDemoViewModel, document: Document) { + + val element: Element? = document.getElementById("details") + var htmlString = "" + if(element != null) { + htmlString = element.html() + } + val htmlSpannableString = Html.fromHtml(htmlString, null, BulletHandler()) + val spannableBuilder = SpannableStringBuilder(htmlSpannableString) + val bulletSpans = spannableBuilder.getSpans(0, spannableBuilder.length, BulletSpan::class.java) + val bulletRadius = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 3.toFloat(), + LocalContext.current.resources.displayMetrics + ).toInt() + val gapWidth = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 8.toFloat(), + LocalContext.current.resources.displayMetrics + ).toInt() + val customTypeface = ResourcesCompat.getFont(LocalContext.current, R.font.ibm_plex_sans_regular) + bulletSpans.forEach { + val start = spannableBuilder.getSpanStart(it) + val end = spannableBuilder.getSpanEnd(it) + spannableBuilder.removeSpan(it) + spannableBuilder.setSpan( + CustomBulletSpan(bulletRadius = bulletRadius, gapWidth = gapWidth), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + } + if (htmlString.isNotEmpty()) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(top = 0.dp, bottom = 12.dp, start = 16.dp, end = 16.dp) + ) { + AndroidView( + factory = { context -> + TextView(context).apply { + textSize = 16f + setTextColor(Variables.mainDefault.toArgb()) + setTypeface(customTypeface) + } + }, + update = { textView -> + textView.text = spannableBuilder + } + ) + } + } +} + +@Composable +fun SettingMoreInfoRecommendationTip(viewModel: AIDataCaptureDemoViewModel, document: Document) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(top = 0.dp, bottom = 12.dp, start = 16.dp, end = 16.dp) + ) { + val element: Element? = document.getElementById("recommendation") + var recommendHtmlString = "" + if(element != null) { + recommendHtmlString = element.html() + } + val htmlSpannableString = Html.fromHtml(recommendHtmlString, null, BulletHandler()) + val spannableBuilder = SpannableStringBuilder(htmlSpannableString) + val bulletSpans = spannableBuilder.getSpans(0, spannableBuilder.length, BulletSpan::class.java) + val bulletRadius = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 3.toFloat(), + LocalContext.current.resources.displayMetrics + ).toInt() + val gapWidth = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 8.toFloat(), + LocalContext.current.resources.displayMetrics + ).toInt() + val customTypeface = ResourcesCompat.getFont(LocalContext.current, R.font.ibm_plex_sans_regular) + bulletSpans.forEach { + val start = spannableBuilder.getSpanStart(it) + val end = spannableBuilder.getSpanEnd(it) + spannableBuilder.removeSpan(it) + spannableBuilder.setSpan( + CustomBulletSpan(bulletRadius = bulletRadius, gapWidth = gapWidth), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + } + if (recommendHtmlString.isNotEmpty()) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(top = 0.dp, bottom = 12.dp, start = 16.dp, end = 16.dp) + ) { + AndroidView( + factory = { context -> + TextView(context).apply { + textSize = 16f + setTextColor(Variables.mainDefault.toArgb()) + setTypeface(customTypeface) + } + }, + update = { textView -> + textView.text = spannableBuilder + } + ) + } + } + HorizontalDivider(modifier = Modifier.fillMaxWidth(), thickness = 2.dp) + val elementTip: Element? = document.getElementById("tip") + var tipHtmlString = AnnotatedString("") + if(elementTip != null) { + tipHtmlString = AnnotatedString.fromHtml(elementTip.html()) + } + Text( + text = tipHtmlString, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainDefault, + ) + ) + + } +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/SingleResultScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/SingleResultScreen.kt new file mode 100644 index 0000000..803c6c9 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/SingleResultScreen.kt @@ -0,0 +1,355 @@ +package com.zebra.aidatacapturedemo.ui.view + +import android.content.Context +import android.view.WindowManager +import android.view.WindowMetrics +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat.getSystemService +import androidx.navigation.NavHostController +import coil.compose.rememberAsyncImagePainter +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.model.FileUtils.Companion.loadOcrBarcodeCaptureSessionDataFromPrefs +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import kotlin.math.min + +/** + * SingleResultScreen is a Composable function that displays the captured image along with the + * OCR or Barcode results. It retrieves the captured image from the session data, calculates + * the appropriate scaling and positioning for the bounding boxes, + * and draws them on top of the image. The screen also handles back navigation to return + * to the list of results. + */ +private const val TAG = "OCRBarcodeResultCapturedScreen" + +@Composable +fun SingleResultScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavHostController, + innerPadding: PaddingValues, + context: Context, + resultRowData: ResultRowData +) { + val uiState = viewModel.uiState.collectAsState().value + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + viewModel.updateAppBarTitle(stringResource(R.string.back_to_all_results)) + val sessionData = loadOcrBarcodeCaptureSessionDataFromPrefs( + context, + uiState.ocrBarcodeCaptureSessionIndex.toString() + ) + val capturedBitmap = sessionData?.captureImage?.let { base64String -> + if (base64String.isNotEmpty()) { + val bytes = android.util.Base64.decode(base64String, android.util.Base64.DEFAULT) + android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + } else null + } + if (capturedBitmap == null) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator() + } + } else { + // GET DEVICE RESOLUTION: + val displayMetrics = LocalContext.current.resources.displayMetrics + val displayMetricsDensity = displayMetrics.density + + val windowManager = getSystemService(context, WindowManager::class.java) + val windowMetrics: WindowMetrics = windowManager.currentWindowMetrics + + val displayTotalWidthInPx = windowMetrics.bounds.width() + val displayTotalHeightInPx = windowMetrics.bounds.height() + + // TOP STATUS BAR + val displayStatusBarPaddingValues = WindowInsets.statusBars.asPaddingValues() + val displayStatusBarHeightInDp = displayStatusBarPaddingValues.calculateTopPadding() + val displayStatusBarHeightInPx = displayStatusBarHeightInDp.value * displayMetricsDensity + + // BOTTOM NAVIGATION BAR + val displayNavigationBarPaddingValues = WindowInsets.navigationBars.asPaddingValues() + val displayNavigationBarHeightInDp = + displayNavigationBarPaddingValues.calculateBottomPadding() + val displayNavigationBarHeightInPx = + displayNavigationBarHeightInDp.value * displayMetricsDensity + + val availableHeightInPx = + displayTotalHeightInPx.toFloat() - displayStatusBarHeightInPx - displayNavigationBarHeightInPx + + // The following computed values are used for drawing Bbox overlay on the preview + val scaler = min( + displayTotalWidthInPx.toFloat() / capturedBitmap.width.toFloat(), + availableHeightInPx / capturedBitmap.height.toFloat() + ) + val scaledWidth = scaler * capturedBitmap.width.toFloat() + val scaledHeight = scaler * capturedBitmap.height.toFloat() + val gapX = (displayTotalWidthInPx - scaledWidth) / 2f + val gapY = (availableHeightInPx - scaledHeight) / 2f + + + Box( // Bottom layer + modifier = Modifier + .fillMaxSize() + .padding( + top = displayStatusBarHeightInDp, + bottom = displayNavigationBarHeightInDp + ) + .background(color = Color.Black) + ) { + + // CAPTURED IMAGE + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Image( + painter = rememberAsyncImagePainter(capturedBitmap), + contentDescription = "Captured Image", + contentScale = ContentScale.Fit + ) + } + if (resultRowData.isBarcode) { + // Draw Barcode results + DrawSingleBarcodeResult( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity, + resultRowData + ) + + } else { + // Draw OCR results + DrawSingleOCRResultWithTextSizeScaling( + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity, + displayTotalHeightInPx = displayTotalHeightInPx, + displayTotalWidthInPx = displayTotalWidthInPx, + resultRowData + ) + } + } + } +} + +@Composable +fun DrawSingleOCRResultWithTextSizeScaling( + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float, + displayTotalHeightInPx: Int, + displayTotalWidthInPx: Int, + resultRowData: ResultRowData +) { + Canvas( // Layer 3 + modifier = Modifier + .fillMaxSize() + ) { + val bBoxTop = resultRowData.boundingBox.top.toFloat() + val bBoxLeft = resultRowData.boundingBox.left.toFloat() + val bBoxBottom = resultRowData.boundingBox.bottom.toFloat() + val bBoxRight = resultRowData.boundingBox.right.toFloat() + + var scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX + var scaledBBoxTopInPx = (scaler * bBoxTop) + gapY + var scaledBBoxRightInPx = (scaler * bBoxRight) + gapX + var scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + + // Define the size and position of the rectangle + var rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + var rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + + // This is preventing the Text to show too small on the drawing + if (rectangleHeight <= 20f || rectangleWidth <= 20f) { + + // Firstly, try increase the BBox Height by 40Px + scaledBBoxTopInPx -= 20f + + // Make sure, the scaling fit within the Screen at Top. + if (scaledBBoxTopInPx < 0) { + scaledBBoxTopInPx = 0f + } + + scaledBBoxBottomInPx += 20f + // Make sure, the scaling fit within the Screen at Bottom. + if (scaledBBoxBottomInPx > displayTotalHeightInPx.toFloat()) { + scaledBBoxBottomInPx = displayTotalHeightInPx.toFloat() + } + + // recalculate the height + rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + + // Secondly, try increase the BBox Width by 40Px + scaledBBoxLeftInPx -= 20f + + // Make sure, the scaling fit within the Screen at Left. + if (scaledBBoxLeftInPx < 0) { + scaledBBoxLeftInPx = 0f + } + + scaledBBoxRightInPx += 20f + // Make sure, the scaling fit within the Screen at Right. + if (scaledBBoxRightInPx > displayTotalWidthInPx.toFloat()) { + scaledBBoxRightInPx = displayTotalWidthInPx.toFloat() + } + + // recalculate the Width + rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + } + + val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + + // Draw the filled rectangle + drawRect( + color = Color(0xBF000000), + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight) + ) + + // Draw the border over the filled rectangle + drawRect( + color = Color(0xFFFF7B00), + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (1f * displayMetricsDensity)) + ) + + // Prepare to draw the text + val paint = android.graphics.Paint().apply { + color = android.graphics.Color.WHITE + textAlign = android.graphics.Paint.Align.CENTER + } + + // Calculate the maximum text size that fits in the rectangle + val padding = 0.5f * displayMetricsDensity // Padding from the border + var textSize = 2f + + // Incrementally increase text size until it just fits + do { + paint.textSize = textSize + val textWidth = paint.measureText(resultRowData.text) + val textHeight = paint.descent() - paint.ascent() + if (textWidth + padding * 2 <= rectangleWidth && textHeight + padding * 2 <= rectangleHeight) { + textSize += 1f + } else { + break + } + } while (true) + + // Adjust the text size to be slightly smaller + paint.textSize = textSize - 1f + + // Calculate the position to draw the text + val textOffsetX = topLeftOffset.x + rectangleWidth / 2 + val textOffsetY = + topLeftOffset.y + rectangleHeight / 2 - (paint.ascent() + paint.descent()) / 2 + + // Draw the text using nativeCanvas + drawContext.canvas.nativeCanvas.drawText( + resultRowData.text, + textOffsetX, + textOffsetY, + paint + ) + } +} + +@Composable +fun DrawSingleBarcodeResult( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float, + resultRowData: ResultRowData +) { + Canvas( + modifier = Modifier + .fillMaxSize() + ) { + val bBoxTop = resultRowData.boundingBox.top.toFloat() + val bBoxLeft = resultRowData.boundingBox.left.toFloat() + val bBoxBottom = resultRowData.boundingBox.bottom.toFloat() + val bBoxRight = resultRowData.boundingBox.right.toFloat() + + val scaledBBoxLeftInPx = (scaler * bBoxLeft) + gapX + val scaledBBoxTopInPx = (scaler * bBoxTop) + gapY + val scaledBBoxRightInPx = (scaler * bBoxRight) + gapX + val scaledBBoxBottomInPx = (scaler * bBoxBottom) + gapY + + // Define the size and position of the rectangle + val rectangleWidth = scaledBBoxRightInPx - scaledBBoxLeftInPx + val rectangleHeight = scaledBBoxBottomInPx - scaledBBoxTopInPx + val topLeftOffset = Offset(scaledBBoxLeftInPx, scaledBBoxTopInPx) + + drawRect( + color = Color.Green, + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (1f * displayMetricsDensity)) + ) + } + + // Draw Decoded Text if found + val bBoxLeft = resultRowData.boundingBox.left.toFloat() + val bBoxBottom = resultRowData.boundingBox.bottom.toFloat() + + val scaledBBoxLeftInDp = (((scaler * bBoxLeft) + gapX) / displayMetricsDensity).dp + val scaledBBoxBottomInDp = (((scaler * bBoxBottom) + gapY) / displayMetricsDensity).dp + + if (resultRowData.text != "") { + Text( + text = resultRowData.text, + fontSize = 10.sp, + color = Color.White, + style = TextStyle( + platformStyle = PlatformTextStyle( + includeFontPadding = false + ) + ), + modifier = Modifier + .offset(x = scaledBBoxLeftInDp, y = scaledBBoxBottomInDp + 2.dp) + .background(Color(0xBF000000)) + .padding(2.dp) + ) + } +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/BarcodeFindFilterHomeScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/BarcodeFindFilterHomeScreen.kt new file mode 100644 index 0000000..fa55613 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/BarcodeFindFilterHomeScreen.kt @@ -0,0 +1,497 @@ +package com.zebra.aidatacapturedemo.ui.view.filters + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.AdvancedFilterOption +import com.zebra.aidatacapturedemo.data.BarcodeFilterData +import com.zebra.aidatacapturedemo.ui.view.ModalLoadingOverlay +import com.zebra.aidatacapturedemo.ui.view.Screen +import com.zebra.aidatacapturedemo.ui.view.Variables +import com.zebra.aidatacapturedemo.ui.view.checkIfScreenExistsInStack +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +@Composable +fun BarcodeFindFilterHomeScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavHostController, + innerPadding: PaddingValues +) { + val uiState = viewModel.uiState.collectAsState().value + + var selectedAdvancedFilterOptionList by remember { mutableStateOf(uiState.barcodeFilterData.selectedAdvancedFilterOptionList) } + + var localSelectedAdvancedFilterOptionListCopy = remember(selectedAdvancedFilterOptionList) { + mutableStateListOf().apply { + addAll(selectedAdvancedFilterOptionList) + } + } + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + + viewModel.updateAppBarTitle(stringResource(R.string.barcode_filter_title)) + + uiState.toastMessage?.let { + viewModel.toast(it) + viewModel.updateToastMessage(message = null) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(innerPadding), + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(start = 20.dp, top = 12.dp, end = 8.dp, bottom = 12.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Advanced: Character match + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = (localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.CHARACTER_MATCH + )), + onCheckedChange = { isChecked -> + if (isChecked) { + // Make sure the list doesn't contain the Preset + if (!localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.CHARACTER_MATCH + ) + ) { + localSelectedAdvancedFilterOptionListCopy.add(AdvancedFilterOption.CHARACTER_MATCH) + } + } else { + // Make sure the list contains the Preset + if (localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.CHARACTER_MATCH + ) + ) { + localSelectedAdvancedFilterOptionListCopy.remove( + AdvancedFilterOption.CHARACTER_MATCH + ) + } + } + }, + colors = CheckboxDefaults.colors( + checkmarkColor = Color.White, + checkedColor = Variables.mainPrimary, + uncheckedColor = Variables.mainSubtle + ) + ) + Spacer(modifier = Modifier.width(width = 4.dp)) + Row(modifier = Modifier.clickable { + if (!localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.CHARACTER_MATCH + ) + ) { + localSelectedAdvancedFilterOptionListCopy.add(AdvancedFilterOption.CHARACTER_MATCH) + } + + updateBarcodeFilterDataChanges( + oldBarcodeFilterData = uiState.barcodeFilterData, viewModel = viewModel, + modifiedSelectedAdvancedFilterOptionList = localSelectedAdvancedFilterOptionListCopy + ) + navController.navigate(route = Screen.CharacterMatchFilter.route) + } + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Character match", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Filter to match specific characters, e.g., starts with, contains, or exact match", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + Spacer(modifier = Modifier.width(width = 8.dp)) + Image( + painter = painterResource(id = R.drawable.ic_right_exapand), + contentDescription = "image description", + contentScale = ContentScale.None + ) + } + } + + // Advanced: String length + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = (localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.STRING_LENGTH + )), + onCheckedChange = { isChecked -> + if (isChecked) { + // Make sure the list doesn't contain the Preset + if (!localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.STRING_LENGTH + ) + ) { + localSelectedAdvancedFilterOptionListCopy.add(AdvancedFilterOption.STRING_LENGTH) + } + } else { + // Make sure the list contains the Preset + if (localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.STRING_LENGTH + ) + ) { + localSelectedAdvancedFilterOptionListCopy.remove( + AdvancedFilterOption.STRING_LENGTH + ) + } + } + }, + colors = CheckboxDefaults.colors( + checkmarkColor = Color.White, + checkedColor = Variables.mainPrimary, + uncheckedColor = Variables.mainSubtle + ) + ) + Spacer(modifier = Modifier.width(width = 4.dp)) + Row(modifier = Modifier.clickable { + if (!localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.STRING_LENGTH + ) + ) { + localSelectedAdvancedFilterOptionListCopy.add(AdvancedFilterOption.STRING_LENGTH) + } + + updateBarcodeFilterDataChanges( + oldBarcodeFilterData = uiState.barcodeFilterData, viewModel = viewModel, + modifiedSelectedAdvancedFilterOptionList = localSelectedAdvancedFilterOptionListCopy + ) + + navController.navigate(route = Screen.StringLengthFilter.route) + } + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "String length", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Filter by min/max limits", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + Spacer(modifier = Modifier.width(width = 8.dp)) + Image( + painter = painterResource(id = R.drawable.ic_right_exapand), + contentDescription = "image description", + contentScale = ContentScale.None + ) + } + } +// } + + } + + // Bottom Action Buttons + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + verticalArrangement = Arrangement.spacedBy(36.dp) + ) { + + // Setting screen shortcut row + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background( + color = Variables.colorsSurfaceSelected, + shape = RoundedCornerShape(size = Variables.radiusMinimal) + ), + horizontalArrangement = Arrangement.spacedBy(10.dp) + + ) { + Row( + modifier = Modifier + .padding( + start = Variables.spacingLarge, + top = Variables.spacingSmall, + end = Variables.spacingLarge, + bottom = Variables.spacingSmall + ), + horizontalArrangement = Arrangement.spacedBy( + Variables.spacingNone, + Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "Visit settings to toggle barcode types.", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextDefault, + ), + modifier = Modifier + .weight(1f) + ) + + Box( + modifier = Modifier + .border( + width = 2.dp, + color = Variables.colorsMainLight, + shape = RoundedCornerShape(size = 4.dp) + ) + .wrapContentWidth() + .wrapContentHeight() + .background( + color = Variables.colorsMainSubtle, + shape = RoundedCornerShape(size = 4.dp) + ) + .padding( + start = Variables.spacingSmall, + top = Variables.spacingSmall, + end = Variables.spacingSmall, + bottom = Variables.spacingSmall + ) + .clickable { + updateBarcodeFilterDataChanges( + oldBarcodeFilterData = uiState.barcodeFilterData, + viewModel = viewModel, + modifiedSelectedAdvancedFilterOptionList = localSelectedAdvancedFilterOptionListCopy + ) + + navController.navigate(route = Screen.DemoSetting.route) + } + ) { + Text( + text = "Go", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.stateDefaultEnabled, + textAlign = TextAlign.Center, + ), + modifier = Modifier.padding( + start = Variables.spacingSmall, + end = Variables.spacingSmall + ) + ) + } + } + } + Row( + modifier = Modifier + .padding(vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Spacer(modifier = Modifier.width(16.dp)) + // Cancel Button + Button( + onClick = { + viewModel.handleBackButton(navController) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.Black + ), + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .weight(1f) + .height(48.dp), + border = BorderStroke(1.dp, Variables.mainLight), + ) { + Text( + text = "Cancel", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.mainDefault, + textAlign = TextAlign.Center, + ) + ) + } + + // Save Button + Button( + onClick = { + updateBarcodeFilterDataChanges( + oldBarcodeFilterData = uiState.barcodeFilterData, viewModel = viewModel, + modifiedSelectedAdvancedFilterOptionList = localSelectedAdvancedFilterOptionListCopy + ) + + viewModel.handleBackButton(navController) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Variables.mainPrimary, + contentColor = Variables.stateDefaultEnabled + ), + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .weight(1f) + .height(48.dp) + ) { + Text( + text = "Save", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.stateDefaultEnabled, + textAlign = TextAlign.Center, + ) + ) + } + Spacer(modifier = Modifier.width(16.dp)) + } + } + } + + var isPreviewScreenExistsInBackStack by remember { mutableStateOf(false) } + + // There are 2 ways a user may navigate to this screen: + // #1 Route: DemoStartScreen -> Start Scan -> CameraPreviewScreen -> Filter Menu -> Barcode Filter -> BarcodeFindFilterHomeScreen + // #2 Route: DemoStartScreen -> Filter Menu -> Barcode Filter -> BarcodeFindFilterHomeScreen + // Now, a user may click Go button to navigate DemoSettingsScreen and come back to the same BarcodeFindFilterHomeScreen. + // During Route #1, uniquely identify this and check if Loading screen is required for Model init + LaunchedEffect(key1 = "Make Barcode Symbology view expand") { + // Check if Screen.Preview exists inside the navigation Controller Stack. + if (checkIfScreenExistsInStack(navController, Screen.Preview.route)) { + isPreviewScreenExistsInBackStack = true + } + } + + LoadingScreen( + viewModel, + navController, + uiState = uiState, + isPreviewScreenExistsInBackStack = isPreviewScreenExistsInBackStack + ) +} + +@Composable +private fun LoadingScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavHostController, + uiState: AIDataCaptureDemoUiState, + isPreviewScreenExistsInBackStack: Boolean +) { + if (isPreviewScreenExistsInBackStack) { + + var areModelsReady by remember { mutableStateOf(false) } + // model re-init required here + if (uiState.isOCRModelEnabled && uiState.isBarcodeModelEnabled) { + if (uiState.isOcrModelDemoReady && uiState.isBarcodeModelDemoReady) { + areModelsReady = true + } + } else if (uiState.isOCRModelEnabled && !uiState.isBarcodeModelEnabled) { + if (uiState.isOcrModelDemoReady) { + areModelsReady = true + } + } else if (uiState.isBarcodeModelEnabled && !uiState.isOCRModelEnabled) { + if (uiState.isBarcodeModelDemoReady) { + areModelsReady = true + } + } + + if (!areModelsReady) { + ModalLoadingOverlay( + onDismissRequest = { + viewModel.handleBackButton(navController = navController) + } + ) + } + } +} + +private fun updateBarcodeFilterDataChanges( + oldBarcodeFilterData: BarcodeFilterData, + viewModel: AIDataCaptureDemoViewModel, + modifiedSelectedAdvancedFilterOptionList: SnapshotStateList +) { + oldBarcodeFilterData.selectedAdvancedFilterOptionList = modifiedSelectedAdvancedFilterOptionList + viewModel.updateBarcodeFilterData(barcodeFilterData = oldBarcodeFilterData) +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/CharacterMatchFilterScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/CharacterMatchFilterScreen.kt new file mode 100644 index 0000000..21c8ce5 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/CharacterMatchFilterScreen.kt @@ -0,0 +1,567 @@ +package com.zebra.aidatacapturedemo.ui.view.filters + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.TextSelectionColors +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +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.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.CharacterMatchData +import com.zebra.aidatacapturedemo.data.CharacterMatchFilterOption +import com.zebra.aidatacapturedemo.data.DetectionLevel +import com.zebra.aidatacapturedemo.data.FilterType +import com.zebra.aidatacapturedemo.ui.view.Variables +import com.zebra.aidatacapturedemo.ui.view.Variables.mainPrimary +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +@Composable +fun CharacterMatchFilterScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavHostController, + innerPadding: PaddingValues +) { + val uiState = viewModel.uiState.collectAsState().value + + var level by remember { + mutableStateOf( + if (uiState.selectedFilterType == FilterType.OCR_FILTER) { + uiState.ocrFilterData.selectedCharacterMatchFilterData.detectionLevel + } else { + uiState.barcodeFilterData.selectedCharacterMatchFilterData.detectionLevel + } + ) + } + + var type by remember { + mutableStateOf( + if (uiState.selectedFilterType == FilterType.OCR_FILTER) { + uiState.ocrFilterData.selectedCharacterMatchFilterData.type + } else { + uiState.barcodeFilterData.selectedCharacterMatchFilterData.type + } + ) + } + + var startsWithString by remember { + mutableStateOf( + if (uiState.selectedFilterType == FilterType.OCR_FILTER) { + uiState.ocrFilterData.selectedCharacterMatchFilterData.startsWithStringList.joinToString() + } else { + uiState.barcodeFilterData.selectedCharacterMatchFilterData.startsWithStringList.joinToString() + } + ) + } // Default separator is a comma + + var containsString by remember { + mutableStateOf( + if (uiState.selectedFilterType == FilterType.OCR_FILTER) { + uiState.ocrFilterData.selectedCharacterMatchFilterData.containsStringList.joinToString() + } else { + uiState.barcodeFilterData.selectedCharacterMatchFilterData.containsStringList.joinToString() + } + ) + } // Default separator is a comma + + var exactMatchString by remember { + mutableStateOf( + if (uiState.selectedFilterType == FilterType.OCR_FILTER) { + uiState.ocrFilterData.selectedCharacterMatchFilterData.exactMatchStringList.joinToString() + } else { + uiState.barcodeFilterData.selectedCharacterMatchFilterData.exactMatchStringList.joinToString() + } + ) + } // Default separator is a comma + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + + viewModel.updateAppBarTitle(stringResource(R.string.ocr_filter_character_match_title)) + + uiState.toastMessage?.let { + viewModel.toast(it) + viewModel.updateToastMessage(message = null) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(innerPadding), + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(top = 16.dp, bottom = 16.dp) + .verticalScroll(rememberScrollState()) + ) { + + if (uiState.selectedFilterType == FilterType.OCR_FILTER) { + // Word vs Line Level Selection Row + Row( + modifier = Modifier + .wrapContentWidth() + .align(Alignment.CenterHorizontally) + .background(color = Variables.mainLight), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .padding(4.dp) + .clickable { + level = DetectionLevel.WORD + } + .then( + if (level == DetectionLevel.WORD) { + Modifier.background( + color = Variables.surfaceDefault, + shape = RoundedCornerShape(size = Variables.radiusMinimal) + ) + } else { + Modifier + } + ) + ) { + Text( + text = "Word Level", + style = TextStyle( + fontSize = Variables.TypefaceFontSize14, + lineHeight = Variables.TypefaceLineHeight20, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = if (level == DetectionLevel.WORD) { + Variables.colorsTextDefault + } else { + Variables.mainSubtle + }, + textAlign = TextAlign.Center, + ), + modifier = Modifier + .padding( + start = Variables.spacingLarge, + top = Variables.spacingMinimum, + end = Variables.spacingLarge, + bottom = Variables.spacingMinimum + ) + ) + } + + Box( + modifier = Modifier + .padding(4.dp) + .clickable { + level = + DetectionLevel.LINE + } + .then( + if (level == DetectionLevel.LINE) { + Modifier.background( + color = Variables.surfaceDefault, + shape = RoundedCornerShape(size = Variables.radiusMinimal) + ) + } else { + Modifier + } + ) + ) { + Text( + text = "Line Level", + style = TextStyle( + fontSize = Variables.TypefaceFontSize14, + lineHeight = Variables.TypefaceLineHeight20, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = if (level == DetectionLevel.LINE) { + Variables.colorsTextDefault + } else { + Variables.mainSubtle + }, + textAlign = TextAlign.Center, + ), + modifier = Modifier + .padding( + start = Variables.spacingLarge, + top = Variables.spacingMinimum, + end = Variables.spacingLarge, + bottom = Variables.spacingMinimum + ) + ) + } + } + } + Spacer(modifier = Modifier.height(16.dp)) + + // Starts with + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 16.dp) + .clickable { + type = CharacterMatchFilterOption.STARTS_WITH + } + ) { + RadioButton( + selected = (type == CharacterMatchFilterOption.STARTS_WITH), + onClick = null, + colors = RadioButtonDefaults.colors( + selectedColor = mainPrimary, + unselectedColor = Variables.mainDefault + ) + ) + Spacer(modifier = Modifier.width(width = 8.dp)) + Column { + Text( + text = "Starts with", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Filters results starting with specific text (e.g., \"45\" or \"P\")", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + } + Spacer(modifier = Modifier.height(height = 4.dp)) + if (type == CharacterMatchFilterOption.STARTS_WITH) { + + InputTextField( + stringValue = startsWithString, + onStringValueChange = { startsWithString = it } + ) + Spacer(modifier = Modifier.height(16.dp)) + } else { + Spacer(modifier = Modifier.height(16.dp)) + } + + // Contains + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 16.dp) + .clickable { + type = CharacterMatchFilterOption.CONTAINS + } + ) { + RadioButton( + selected = (type == CharacterMatchFilterOption.CONTAINS), + onClick = null, + colors = RadioButtonDefaults.colors( + selectedColor = mainPrimary, + unselectedColor = Variables.mainDefault + ) + ) + Spacer(modifier = Modifier.width(width = 8.dp)) + Column { + Text( + text = "Contains", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Filters results containing specific text anywhere in the string (e.g., \"2025\")", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + } + Spacer(modifier = Modifier.height(height = 4.dp)) + if (type == CharacterMatchFilterOption.CONTAINS) { + InputTextField( + stringValue = containsString, + onStringValueChange = { containsString = it } + ) + Spacer(modifier = Modifier.height(16.dp)) + } else { + Spacer(modifier = Modifier.height(16.dp)) + } + + // Exact match + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 16.dp) + .clickable { + type = CharacterMatchFilterOption.EXACT_MATCH + } + ) { + RadioButton( + selected = (type == CharacterMatchFilterOption.EXACT_MATCH), + onClick = null, + colors = RadioButtonDefaults.colors( + selectedColor = mainPrimary, + unselectedColor = Variables.mainDefault + ) + ) + Spacer(modifier = Modifier.width(width = 8.dp)) + Column { + Text( + text = "Exact match", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Only shows results that exactly match your input", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + } + Spacer(modifier = Modifier.height(height = 4.dp)) + if (type == CharacterMatchFilterOption.EXACT_MATCH) { + InputTextField( + stringValue = exactMatchString, + onStringValueChange = { exactMatchString = it } + ) + Spacer(modifier = Modifier.height(16.dp)) + } else { + Spacer(modifier = Modifier.height(16.dp)) + } + } + + + // Bottom Action Buttons + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Spacer(modifier = Modifier.width(16.dp)) + // Cancel Button + Button( + onClick = { + viewModel.handleBackButton(navController) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.Black + ), + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .weight(1f) + .height(48.dp), + border = BorderStroke(1.dp, Variables.mainLight), + ) { + Text( + text = "Cancel", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.mainDefault, + textAlign = TextAlign.Center, + ) + ) + } + + // Save Button + Button( + onClick = { + viewModel.updateToastMessage("Save was successful.") + + if (uiState.selectedFilterType == FilterType.OCR_FILTER) { + val defaultOcrFilterData = uiState.ocrFilterData + defaultOcrFilterData.selectedCharacterMatchFilterData = CharacterMatchData( + detectionLevel = level, + type = type, + startsWithStringList = startsWithString.split(",") + .map { it.trim() }, + containsStringList = containsString.split(",").map { it.trim() }, + exactMatchStringList = exactMatchString.split(",").map { it.trim() } + ) + viewModel.updateOcrFilterData(ocrFilterData = defaultOcrFilterData) + } else { + val defaultBarcodeFilterData = uiState.barcodeFilterData + defaultBarcodeFilterData.selectedCharacterMatchFilterData = + CharacterMatchData( + detectionLevel = level, + type = type, + startsWithStringList = startsWithString.split(",") + .map { it.trim() }, + containsStringList = containsString.split(",").map { it.trim() }, + exactMatchStringList = exactMatchString.split(",").map { it.trim() } + ) + viewModel.updateBarcodeFilterData(barcodeFilterData = defaultBarcodeFilterData) + } + viewModel.handleBackButton(navController) + }, + colors = ButtonDefaults.buttonColors( + containerColor = mainPrimary, + contentColor = Variables.stateDefaultEnabled + ), + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .weight(1f) + .height(48.dp) + ) { + Text( + text = "Save", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.stateDefaultEnabled, + textAlign = TextAlign.Center, + ) + ) + } + Spacer(modifier = Modifier.width(16.dp)) + } + } + +} + +@Composable +private fun InputTextField( + stringValue: String, + onStringValueChange: (String) -> Unit, + showTextFieldHint: Boolean = true +) { + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(color = Variables.colorsSurfaceCool) + ) { + Column( + modifier = Modifier.padding( + start = 40.dp, + end = 16.dp, + top = 14.dp, + bottom = 14.dp + ) + ) { + OutlinedTextField( + value = stringValue, + onValueChange = { onStringValueChange(it) }, + colors = OutlinedTextFieldDefaults.colors( + selectionColors = TextSelectionColors( + handleColor = mainPrimary, + backgroundColor = mainPrimary + ), + cursorColor = mainPrimary, + focusedContainerColor = Variables.surfaceDefault, + unfocusedContainerColor = Variables.surfaceDefault, + focusedBorderColor = mainPrimary, + unfocusedBorderColor = Variables.borderDefault, + ), + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(size = Variables.radiusMinimal), + trailingIcon = { + if (stringValue.isNotEmpty()) { + IconButton(onClick = { + onStringValueChange("") + }) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_close_black), + contentDescription = "Clear text", + tint = Variables.mainSubtle + ) + } + } + } + ) + + if (showTextFieldHint) { + Spacer(modifier = Modifier.padding(top = 4.dp)) + Text( + text = "Use comma’s for multiple options", + style = TextStyle( + fontSize = 10.sp, + lineHeight = Variables.TypefaceLineHeight24, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + } + } +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/CharacterTypeFilterScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/CharacterTypeFilterScreen.kt new file mode 100644 index 0000000..01e67fc --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/CharacterTypeFilterScreen.kt @@ -0,0 +1,463 @@ +package com.zebra.aidatacapturedemo.ui.view.filters + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +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.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.CharacterTypeFilterOption +import com.zebra.aidatacapturedemo.ui.view.Variables +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +@Composable +fun CharacterTypeFilterScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavHostController, + innerPadding: PaddingValues +) { + val uiState = viewModel.uiState.collectAsState().value + var selectedCharacterTypeFilterOptionList by remember { mutableStateOf(uiState.ocrFilterData.selectedCharacterTypeFilterOptionList) } + + val localSelectedCharacterTypeFilterOptionListCopy = + remember(selectedCharacterTypeFilterOptionList) { + mutableStateListOf().apply { + addAll(selectedCharacterTypeFilterOptionList) + } + } + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + + viewModel.updateAppBarTitle(stringResource(R.string.ocr_filter_character_type_title)) + + uiState.toastMessage?.let { + viewModel.toast(it) + viewModel.updateToastMessage(message = null) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(innerPadding), + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(start = 20.dp, top = 12.dp, end = 12.dp, bottom = 12.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Select All + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { + if (localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.SELECT_ALL + ) + ) { + localSelectedCharacterTypeFilterOptionListCopy.clear() // clear all the selection + } else { + localSelectedCharacterTypeFilterOptionListCopy.add( + CharacterTypeFilterOption.SELECT_ALL + ) + localSelectedCharacterTypeFilterOptionListCopy.add( + CharacterTypeFilterOption.ALPHA + ) + localSelectedCharacterTypeFilterOptionListCopy.add( + CharacterTypeFilterOption.NUMERIC + ) + localSelectedCharacterTypeFilterOptionListCopy.add( + CharacterTypeFilterOption.INCLUDE_SPECIAL_CHARACTERS + ) + } + }) { + Checkbox( + checked = (localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.SELECT_ALL + )), + onCheckedChange = null, + colors = CheckboxDefaults.colors( + checkmarkColor = Color.White, + checkedColor = Variables.mainPrimary, + uncheckedColor = Variables.mainSubtle + ) + ) + Spacer(modifier = Modifier.width(width = 8.dp)) + Column { + Text( + text = "Select All", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Include alphanumeric and special characters", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + } + + // Alpha + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { + if (localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.ALPHA + ) + ) { + localSelectedCharacterTypeFilterOptionListCopy.remove( + CharacterTypeFilterOption.ALPHA + ) + + // Explicitly handle SelectAll removal + if (localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.SELECT_ALL + ) + ) { + localSelectedCharacterTypeFilterOptionListCopy.remove( + CharacterTypeFilterOption.SELECT_ALL + ) + } + } else { + localSelectedCharacterTypeFilterOptionListCopy.add( + CharacterTypeFilterOption.ALPHA + ) + + // Explicitly handle SelectAll inclusion + if (localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.NUMERIC + ) && + localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.INCLUDE_SPECIAL_CHARACTERS + ) + ) { + localSelectedCharacterTypeFilterOptionListCopy.add( + CharacterTypeFilterOption.SELECT_ALL + ) + } + } + }) { + Checkbox( + checked = (localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.ALPHA + )), + onCheckedChange = null, + colors = CheckboxDefaults.colors( + checkmarkColor = Color.White, + checkedColor = Variables.mainPrimary, + uncheckedColor = Variables.mainSubtle + ) + ) + Spacer(modifier = Modifier.width(width = 8.dp)) + Column { + Text( + text = "Alpha", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Shows results with letters (e.g., \"AaBbCc\")", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + } + + // Numeric + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { + if (localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.NUMERIC + ) + ) { + localSelectedCharacterTypeFilterOptionListCopy.remove( + CharacterTypeFilterOption.NUMERIC + ) + + // Explicitly handle SelectAll removal + if (localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.SELECT_ALL + ) + ) { + localSelectedCharacterTypeFilterOptionListCopy.remove( + CharacterTypeFilterOption.SELECT_ALL + ) + } + } else { + localSelectedCharacterTypeFilterOptionListCopy.add( + CharacterTypeFilterOption.NUMERIC + ) + + // Explicitly handle SelectAll inclusion + if (localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.ALPHA + ) && + localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.INCLUDE_SPECIAL_CHARACTERS + ) + ) { + localSelectedCharacterTypeFilterOptionListCopy.add( + CharacterTypeFilterOption.SELECT_ALL + ) + } + } + }) { + Checkbox( + checked = (localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.NUMERIC + )), + onCheckedChange = null, + colors = CheckboxDefaults.colors( + checkmarkColor = Color.White, + checkedColor = Variables.mainPrimary, + uncheckedColor = Variables.mainSubtle + ) + ) + Spacer(modifier = Modifier.width(width = 8.dp)) + Column { + Text( + text = "Numeric", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Shows results with numbers (e.g., \"12345\")", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + } + + // Include special characters + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { + if (localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.INCLUDE_SPECIAL_CHARACTERS + ) + ) { + localSelectedCharacterTypeFilterOptionListCopy.remove( + CharacterTypeFilterOption.INCLUDE_SPECIAL_CHARACTERS + ) + + // Explicitly handle SelectAll removal + if (localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.SELECT_ALL + ) + ) { + localSelectedCharacterTypeFilterOptionListCopy.remove( + CharacterTypeFilterOption.SELECT_ALL + ) + } + } else { + localSelectedCharacterTypeFilterOptionListCopy.add( + CharacterTypeFilterOption.INCLUDE_SPECIAL_CHARACTERS + ) + + // Explicitly handle SelectAll inclusion + if (localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.ALPHA + ) && + localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.NUMERIC + ) + ) { + localSelectedCharacterTypeFilterOptionListCopy.add( + CharacterTypeFilterOption.SELECT_ALL + ) + } + } + }) { + Checkbox( + checked = (localSelectedCharacterTypeFilterOptionListCopy.contains( + CharacterTypeFilterOption.INCLUDE_SPECIAL_CHARACTERS + )), + onCheckedChange = null, + colors = CheckboxDefaults.colors( + checkmarkColor = Color.White, + checkedColor = Variables.mainPrimary, + uncheckedColor = Variables.mainSubtle + ) + ) + Spacer(modifier = Modifier.width(width = 8.dp)) + Column { + Text( + text = "Include special characters", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Show special characters with Alpha or Numeric selection (e.g., \"$-/@\")", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + } + } + + + // Bottom Action Buttons + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Spacer(modifier = Modifier.width(16.dp)) + // Cancel Button + Button( + onClick = { + viewModel.handleBackButton(navController) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.Black + ), + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .weight(1f) + .height(48.dp), + border = BorderStroke(1.dp, Variables.mainLight), + ) { + Text( + text = "Cancel", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.mainDefault, + textAlign = TextAlign.Center, + ) + ) + } + + // Save Button + Button( + onClick = { + viewModel.updateToastMessage("Save was successful.") + val defaultOcrFilterData = uiState.ocrFilterData + defaultOcrFilterData.selectedCharacterTypeFilterOptionList = + localSelectedCharacterTypeFilterOptionListCopy + viewModel.updateOcrFilterData(ocrFilterData = defaultOcrFilterData) + viewModel.handleBackButton(navController) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Variables.mainPrimary, + contentColor = Variables.stateDefaultEnabled + ), + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .weight(1f) + .height(48.dp) + ) { + Text( + text = "Save", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.stateDefaultEnabled, + textAlign = TextAlign.Center, + ) + ) + } + Spacer(modifier = Modifier.width(16.dp)) + } + } + +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/OCRFindFilterHomeScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/OCRFindFilterHomeScreen.kt new file mode 100644 index 0000000..197c511 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/OCRFindFilterHomeScreen.kt @@ -0,0 +1,624 @@ +package com.zebra.aidatacapturedemo.ui.view.filters + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.AdvancedFilterOption +import com.zebra.aidatacapturedemo.data.OcrFilterData +import com.zebra.aidatacapturedemo.data.OcrRegularFilterOption +import com.zebra.aidatacapturedemo.ui.view.Screen +import com.zebra.aidatacapturedemo.ui.view.Variables +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +@Composable +fun OCRFindFilterHomeScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavHostController, + innerPadding: PaddingValues +) { + val uiState = viewModel.uiState.collectAsState().value + + //START FROM HERE on Local var copy + var selectedRegularFilterOption by remember { mutableStateOf(uiState.ocrFilterData.selectedRegularFilterOption) } + var selectedAdvancedFilterOptionList by remember { mutableStateOf(uiState.ocrFilterData.selectedAdvancedFilterOptionList) } + + val localSelectedAdvancedFilterOptionListCopy = remember(selectedAdvancedFilterOptionList) { + mutableStateListOf().apply { + addAll(selectedAdvancedFilterOptionList) + } + } + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + + viewModel.updateAppBarTitle(stringResource(R.string.ocr_filter_title)) + + uiState.toastMessage?.let { + viewModel.toast(it) + viewModel.updateToastMessage(message = null) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(innerPadding), + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(start = 20.dp, top = 12.dp, end = 8.dp, bottom = 12.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Show All + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clickable { + selectedRegularFilterOption = OcrRegularFilterOption.UNFILTERED + }) { + RadioButton( + selected = (selectedRegularFilterOption == OcrRegularFilterOption.UNFILTERED), + onClick = { + selectedRegularFilterOption = OcrRegularFilterOption.UNFILTERED + }, + colors = RadioButtonDefaults.colors( + selectedColor = Variables.mainPrimary, + unselectedColor = Variables.mainDefault + ) + ) + Spacer(modifier = Modifier.width(width = 8.dp)) + Column { + Text( + text = "Unfiltered", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Displays every available output", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + } + + // Regex + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = (selectedRegularFilterOption == OcrRegularFilterOption.REGEX), + onClick = { + selectedRegularFilterOption = OcrRegularFilterOption.REGEX + }, + colors = RadioButtonDefaults.colors( + selectedColor = Variables.mainPrimary, + unselectedColor = Variables.mainDefault + ) + ) + Spacer(modifier = Modifier.width(width = 8.dp)) + Row(modifier = Modifier.clickable { + + selectedRegularFilterOption = OcrRegularFilterOption.REGEX + + updateOcrFilterDataChanges( + oldOcrFilterData = uiState.ocrFilterData, + viewModel = viewModel, + modifiedRegularFilterOption = selectedRegularFilterOption, + modifiedAdvancedFilterOptionList = localSelectedAdvancedFilterOptionListCopy + ) + + navController.navigate(route = Screen.RegexFilter.route) + } + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Regex", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Returns text strings that match a sequence of characters defined by a RegEx pattern", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + Spacer(modifier = Modifier.width(width = 8.dp)) + Image( + painter = painterResource(id = R.drawable.ic_right_exapand), + contentDescription = "image description", + contentScale = ContentScale.None + ) + } + } + + // Advanced + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = (selectedRegularFilterOption == OcrRegularFilterOption.ADVANCED), + onClick = { + selectedRegularFilterOption = OcrRegularFilterOption.ADVANCED + }, + colors = RadioButtonDefaults.colors( + selectedColor = Variables.mainPrimary, + unselectedColor = Variables.mainDefault, + disabledSelectedColor = Variables.colorsSurfaceDisabled, + disabledUnselectedColor = Variables.colorsSurfaceDisabled + ), + enabled = localSelectedAdvancedFilterOptionListCopy.isNotEmpty() + ) + Spacer(modifier = Modifier.width(width = 8.dp)) + Column { + Text( + text = "Advanced", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Choose from the options below", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + } + + Column( + modifier = Modifier.padding(start = 16.dp, top = 2.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + + // Advanced: Character type + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = (localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.CHARACTER_TYPE + )), + onCheckedChange = { isChecked -> + if (isChecked) { + // Make sure the list doesn't contain the Preset + if (!localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.CHARACTER_TYPE + ) + ) { + localSelectedAdvancedFilterOptionListCopy.add( + AdvancedFilterOption.CHARACTER_TYPE + ) + } + + if (selectedRegularFilterOption != OcrRegularFilterOption.ADVANCED) { + selectedRegularFilterOption = OcrRegularFilterOption.ADVANCED + } + } else { + // Make sure the list contains the Preset + if (localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.CHARACTER_TYPE + ) + ) { + localSelectedAdvancedFilterOptionListCopy.remove( + AdvancedFilterOption.CHARACTER_TYPE + ) + } + + if (localSelectedAdvancedFilterOptionListCopy.isEmpty()) { + selectedRegularFilterOption = OcrRegularFilterOption.UNFILTERED + } + } + }, + colors = CheckboxDefaults.colors( + checkmarkColor = Color.White, + checkedColor = Variables.mainPrimary, + uncheckedColor = Variables.mainSubtle + ) + ) + Spacer(modifier = Modifier.width(width = 4.dp)) + Row(modifier = Modifier.clickable { + selectedRegularFilterOption = OcrRegularFilterOption.ADVANCED + + if (!localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.CHARACTER_TYPE + ) + ) { + localSelectedAdvancedFilterOptionListCopy.add(AdvancedFilterOption.CHARACTER_TYPE) + } + + updateOcrFilterDataChanges( + oldOcrFilterData = uiState.ocrFilterData, + viewModel = viewModel, + modifiedRegularFilterOption = selectedRegularFilterOption, + modifiedAdvancedFilterOptionList = localSelectedAdvancedFilterOptionListCopy + ) + + navController.navigate(route = Screen.CharacterTypeFilter.route) + } + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Character type", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Filter by numbers, letters, and/or special characters", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + Spacer(modifier = Modifier.width(width = 8.dp)) + Image( + painter = painterResource(id = R.drawable.ic_right_exapand), + contentDescription = "image description", + contentScale = ContentScale.None + ) + } + } + + // Advanced: Character match + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = (localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.CHARACTER_MATCH + )), + onCheckedChange = { isChecked -> + if (isChecked) { + // Make sure the list doesn't contain the Preset + if (!localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.CHARACTER_MATCH + ) + ) { + localSelectedAdvancedFilterOptionListCopy.add( + AdvancedFilterOption.CHARACTER_MATCH + ) + } + + if (selectedRegularFilterOption != OcrRegularFilterOption.ADVANCED) { + selectedRegularFilterOption = OcrRegularFilterOption.ADVANCED + } + } else { + // Make sure the list contains the Preset + if (localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.CHARACTER_MATCH + ) + ) { + localSelectedAdvancedFilterOptionListCopy.remove( + AdvancedFilterOption.CHARACTER_MATCH + ) + } + + if (localSelectedAdvancedFilterOptionListCopy.isEmpty()) { + selectedRegularFilterOption = OcrRegularFilterOption.UNFILTERED + } + } + }, + colors = CheckboxDefaults.colors( + checkmarkColor = Color.White, + checkedColor = Variables.mainPrimary, + uncheckedColor = Variables.mainSubtle + ) + ) + Spacer(modifier = Modifier.width(width = 4.dp)) + Row(modifier = Modifier.clickable { + selectedRegularFilterOption = OcrRegularFilterOption.ADVANCED + + if (!localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.CHARACTER_MATCH + ) + ) { + localSelectedAdvancedFilterOptionListCopy.add(AdvancedFilterOption.CHARACTER_MATCH) + } + + updateOcrFilterDataChanges( + oldOcrFilterData = uiState.ocrFilterData, + viewModel = viewModel, + modifiedRegularFilterOption = selectedRegularFilterOption, + modifiedAdvancedFilterOptionList = localSelectedAdvancedFilterOptionListCopy + ) + + navController.navigate(route = Screen.CharacterMatchFilter.route) + } + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Character match", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Filter to match specific characters, e.g., starts with, contains, or exact match", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + Spacer(modifier = Modifier.width(width = 8.dp)) + Image( + painter = painterResource(id = R.drawable.ic_right_exapand), + contentDescription = "image description", + contentScale = ContentScale.None + ) + } + } + + // Advanced: String length + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = (localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.STRING_LENGTH + )), + onCheckedChange = { isChecked -> + if (isChecked) { + // Make sure the list doesn't contain the Preset + if (!localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.STRING_LENGTH + ) + ) { + localSelectedAdvancedFilterOptionListCopy.add( + AdvancedFilterOption.STRING_LENGTH + ) + } + + if (selectedRegularFilterOption != OcrRegularFilterOption.ADVANCED) { + selectedRegularFilterOption = OcrRegularFilterOption.ADVANCED + } + } else { + // Make sure the list contains the Preset + if (localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.STRING_LENGTH + ) + ) { + localSelectedAdvancedFilterOptionListCopy.remove( + AdvancedFilterOption.STRING_LENGTH + ) + } + + if (localSelectedAdvancedFilterOptionListCopy.isEmpty()) { + selectedRegularFilterOption = OcrRegularFilterOption.UNFILTERED + } + } + }, + colors = CheckboxDefaults.colors( + checkmarkColor = Color.White, + checkedColor = Variables.mainPrimary, + uncheckedColor = Variables.mainSubtle + ) + ) + Spacer(modifier = Modifier.width(width = 4.dp)) + Row(modifier = Modifier.clickable { + selectedRegularFilterOption = OcrRegularFilterOption.ADVANCED + + if (!localSelectedAdvancedFilterOptionListCopy.contains( + AdvancedFilterOption.STRING_LENGTH + ) + ) { + localSelectedAdvancedFilterOptionListCopy.add(AdvancedFilterOption.STRING_LENGTH) + } + + updateOcrFilterDataChanges( + oldOcrFilterData = uiState.ocrFilterData, + viewModel = viewModel, + modifiedRegularFilterOption = selectedRegularFilterOption, + modifiedAdvancedFilterOptionList = localSelectedAdvancedFilterOptionListCopy + ) + + navController.navigate(route = Screen.StringLengthFilter.route) + } + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "String length", + style = TextStyle( + fontSize = 14.sp, + lineHeight = Variables.TypefaceLineHeight18, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(700), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = "Filter by min/max limits", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextBody, + ) + ) + } + Spacer(modifier = Modifier.width(width = 8.dp)) + Image( + painter = painterResource(id = R.drawable.ic_right_exapand), + contentDescription = "image description", + contentScale = ContentScale.None + ) + } + } + } + + } + + // Bottom Action Buttons + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Spacer(modifier = Modifier.width(16.dp)) + // Cancel Button + Button( + onClick = { + viewModel.handleBackButton(navController) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.Black + ), + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .weight(1f) + .height(48.dp), + border = BorderStroke(1.dp, Variables.mainLight), + ) { + Text( + text = "Cancel", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.mainDefault, + textAlign = TextAlign.Center, + ) + ) + } + + // Save Button + Button( + onClick = { + updateOcrFilterDataChanges( + oldOcrFilterData = uiState.ocrFilterData, + viewModel = viewModel, + modifiedRegularFilterOption = selectedRegularFilterOption, + modifiedAdvancedFilterOptionList = localSelectedAdvancedFilterOptionListCopy + ) + + viewModel.handleBackButton(navController) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Variables.mainPrimary, + contentColor = Variables.stateDefaultEnabled + ), + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .weight(1f) + .height(48.dp) + ) { + Text( + text = "Save", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.stateDefaultEnabled, + textAlign = TextAlign.Center, + ) + ) + } + Spacer(modifier = Modifier.width(16.dp)) + } + } + +} + +private fun updateOcrFilterDataChanges( + oldOcrFilterData: OcrFilterData, + viewModel: AIDataCaptureDemoViewModel, + modifiedRegularFilterOption: OcrRegularFilterOption, + modifiedAdvancedFilterOptionList: SnapshotStateList +) { + // append the new state changes to the existing data + oldOcrFilterData.selectedRegularFilterOption = modifiedRegularFilterOption + oldOcrFilterData.selectedAdvancedFilterOptionList = modifiedAdvancedFilterOptionList + viewModel.updateOcrFilterData(ocrFilterData = oldOcrFilterData) +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/RegexFilterScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/RegexFilterScreen.kt new file mode 100644 index 0000000..f0298c4 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/RegexFilterScreen.kt @@ -0,0 +1,631 @@ +package com.zebra.aidatacapturedemo.ui.view.filters + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.TextSelectionColors +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.DetectionLevel +import com.zebra.aidatacapturedemo.data.RegexData +import com.zebra.aidatacapturedemo.model.FilterUtils +import com.zebra.aidatacapturedemo.ui.view.Variables +import com.zebra.aidatacapturedemo.ui.view.Variables.mainPrimary +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +@Composable +fun RegexFilterScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavHostController, + innerPadding: PaddingValues +) { + val uiState = viewModel.uiState.collectAsState().value + + var regexDetectionLevel by remember { mutableStateOf(uiState.ocrFilterData.selectedRegexFilterData.detectionLevel) } + var regexDefaultString by remember { mutableStateOf(uiState.ocrFilterData.selectedRegexFilterData.regexDefaultString) } + var regexAdditionalStringList by remember { mutableStateOf(uiState.ocrFilterData.selectedRegexFilterData.regexAdditionalStringList) } + + // Create a Compose-observable state that preserves local changes across recompositions + val localRegexAdditionalStringListCopy = remember(regexAdditionalStringList) { + mutableStateListOf().apply { + addAll(regexAdditionalStringList) + } + } + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + + uiState.toastMessage?.let { + viewModel.toast(it) + viewModel.updateToastMessage(message = null) + } + + viewModel.updateAppBarTitle(stringResource(R.string.ocr_filter_regex_title)) + + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(innerPadding), + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(top = 16.dp, bottom = 16.dp) + .verticalScroll(rememberScrollState()) + ) { + + // Word vs Line Level Selection Row + Row( + modifier = Modifier + .wrapContentWidth() + .align(Alignment.CenterHorizontally) + .background(color = Variables.mainLight), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .padding(4.dp) + .clickable { + regexDetectionLevel = + DetectionLevel.WORD + } + .then( + if (regexDetectionLevel == DetectionLevel.WORD) { + Modifier.background( + color = Variables.surfaceDefault, + shape = RoundedCornerShape(size = Variables.radiusMinimal) + ) + } else { + Modifier + } + ) + ) { + Text( + text = "Word Level", + style = TextStyle( + fontSize = Variables.TypefaceFontSize14, + lineHeight = Variables.TypefaceLineHeight20, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = if (regexDetectionLevel == DetectionLevel.WORD) { + FontWeight(700) + } else { + FontWeight(500) + }, + color = if (regexDetectionLevel == DetectionLevel.WORD) { + Variables.colorsTextDefault + } else { + Variables.mainSubtle + }, + textAlign = TextAlign.Center, + ), + modifier = Modifier + .padding( + start = Variables.spacingLarge, + top = Variables.spacingMinimum, + end = Variables.spacingLarge, + bottom = Variables.spacingMinimum + ) + ) + } + + Box( + modifier = Modifier + .padding(4.dp) + .clickable { + regexDetectionLevel = + DetectionLevel.LINE + } + .then( + if (regexDetectionLevel == DetectionLevel.LINE) { + Modifier.background( + color = Variables.surfaceDefault, + shape = RoundedCornerShape(size = Variables.radiusMinimal) + ) + } else { + Modifier + } + ) + ) { + Text( + text = "Line Level", + style = TextStyle( + fontSize = Variables.TypefaceFontSize14, + lineHeight = Variables.TypefaceLineHeight20, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = if (regexDetectionLevel == DetectionLevel.LINE) { + FontWeight(700) + } else { + FontWeight(500) + }, + color = if (regexDetectionLevel == DetectionLevel.LINE) { + Variables.colorsTextDefault + } else { + Variables.mainSubtle + }, + textAlign = TextAlign.Center, + ), + modifier = Modifier + .padding( + start = Variables.spacingLarge, + top = Variables.spacingMinimum, + end = Variables.spacingLarge, + bottom = Variables.spacingMinimum + ) + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) + + // Default regex row. + // Note: This row view will be always visible, cannot be deleted + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + InputTextFieldDefaultRegex( + stringValue = regexDefaultString, + onStringValueChange = { regexDefaultString = it } + ) + Spacer(modifier = Modifier.height(4.dp)) + } + Spacer(modifier = Modifier.height(16.dp)) + + // Additional Regex Row(s) + localRegexAdditionalStringListCopy.forEachIndexed { index, value -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + + InputTextFieldAdditionalRegex( + stringValue = value, + onStringValueChange = { newValue -> + localRegexAdditionalStringListCopy[index] = newValue + }, + onTrashCanButtonClicked = { + localRegexAdditionalStringListCopy.remove(value) + } + ) + Spacer(modifier = Modifier.height(4.dp)) + } + Spacer(modifier = Modifier.height(16.dp)) + } + + // Add more value Row + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp) + .clickable { + localRegexAdditionalStringListCopy.add("") + } + ) { + Image( + painter = painterResource(id = R.drawable.ic_plus), + contentDescription = "image description", + contentScale = ContentScale.None + ) + + Spacer(modifier = Modifier.width(width = Variables.spacingMinimum)) + + Text( + text = "Add Value", + style = TextStyle( + fontSize = 12.sp, + lineHeight = 16.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.borderPrimaryMain, + textAlign = TextAlign.Center, + ) + ) + } + + } + + + // Bottom Action Buttons + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Spacer(modifier = Modifier.width(16.dp)) + // Cancel Button + Button( + onClick = { + viewModel.handleBackButton(navController) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.Black + ), + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .weight(1f) + .height(48.dp), + border = BorderStroke(1.dp, Variables.mainLight), + ) { + Text( + text = "Cancel", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.mainDefault, + textAlign = TextAlign.Center, + ) + ) + } + + // Save Button + Button( + onClick = { + + if (regexDefaultString.isBlank()) { + viewModel.updateToastMessage(message = "RegEx field cannot be empty") + return@Button + } + + if (FilterUtils.validateRegexSyntax(regexDefaultString) == null) { + viewModel.updateToastMessage(message = "Check RegEx for errors.") + return@Button + } + + var isAdditionalRegexStringInvalid = false + run loop@{ + localRegexAdditionalStringListCopy.forEachIndexed { index, regexString -> + if (regexDefaultString.isBlank() || FilterUtils.validateRegexSyntax( + regexString + ) == null + ) { + viewModel.updateToastMessage(message = "Check RegEx for errors.") + isAdditionalRegexStringInvalid = true + return@loop + } + } + } + + if (isAdditionalRegexStringInvalid) { + return@Button + } + + viewModel.updateToastMessage("Save was successful.") + + val defaultOcrFilterData = uiState.ocrFilterData + defaultOcrFilterData.selectedRegexFilterData = RegexData( + detectionLevel = regexDetectionLevel, + regexDefaultString = regexDefaultString, + regexAdditionalStringList = localRegexAdditionalStringListCopy.filter { it.isNotBlank() } + .toMutableList() + ) + viewModel.updateOcrFilterData(ocrFilterData = defaultOcrFilterData) + + + viewModel.handleBackButton(navController) + }, + colors = ButtonDefaults.buttonColors( + containerColor = mainPrimary, + contentColor = Variables.stateDefaultEnabled + ), + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .weight(1f) + .height(48.dp) + ) { + Text( + text = "Save", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.stateDefaultEnabled, + textAlign = TextAlign.Center, + ) + ) + } + Spacer(modifier = Modifier.width(16.dp)) + } + } + +} + +@Composable +private fun InputTextFieldDefaultRegex( + stringValue: String, + onStringValueChange: (String) -> Unit +) { + var isRegexValid by remember { mutableStateOf(true) } + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(color = Variables.colorsSurfaceCool) + ) { + Column( + modifier = Modifier.padding( + start = 20.dp, + end = 20.dp, + top = 14.dp, + bottom = 10.dp + ) + ) { + OutlinedTextField( + value = stringValue, + onValueChange = { + FilterUtils.validateRegexSyntax(it)?.let { + isRegexValid = true + } ?: run { + isRegexValid = false + } + onStringValueChange(it) + }, + colors = OutlinedTextFieldDefaults.colors( + selectionColors = TextSelectionColors( + handleColor = mainPrimary, + backgroundColor = mainPrimary + ), + cursorColor = mainPrimary, + focusedContainerColor = Variables.surfaceDefault, + unfocusedContainerColor = Variables.surfaceDefault, + focusedBorderColor = if (isRegexValid) { + mainPrimary + } else { + Variables.colorsIconNegative + }, + unfocusedBorderColor = if (isRegexValid) { + Variables.borderDefault + } else { + Variables.colorsIconNegative + }, + ), + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(size = Variables.radiusMinimal), + trailingIcon = { + if (stringValue.isNotEmpty()) { + IconButton(onClick = { + onStringValueChange("") + }) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_close_black), + contentDescription = "Clear text", + tint = Variables.mainSubtle + ) + } + } + } + ) + + Spacer(modifier = Modifier.padding(top = 4.dp)) + + if (isRegexValid) { + Text( + text = "Enter value", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsMainSubtle, + ) + ) + } else { + Text( + text = "Value not valid", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsMainNegative, + ) + ) + } + + } + } +} + +@Composable +private fun InputTextFieldAdditionalRegex( + stringValue: String, + onStringValueChange: (String) -> Unit, + onTrashCanButtonClicked: (Boolean) -> Unit +) { + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(color = Variables.colorsSurfaceCool) + ) { + Column( + modifier = Modifier.padding( + start = 20.dp, + end = 8.dp, + top = 14.dp, + bottom = 10.dp + ), + ) { + var isRegexValid by remember { mutableStateOf(true) } + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = stringValue, + onValueChange = { + FilterUtils.validateRegexSyntax(it)?.let { + isRegexValid = true + } ?: run { + isRegexValid = false + } + onStringValueChange(it) + }, + colors = OutlinedTextFieldDefaults.colors( + selectionColors = TextSelectionColors( + handleColor = mainPrimary, + backgroundColor = mainPrimary + ), + cursorColor = mainPrimary, + focusedContainerColor = Variables.surfaceDefault, + unfocusedContainerColor = Variables.surfaceDefault, + focusedBorderColor = if (isRegexValid) { + mainPrimary + } else { + Variables.colorsIconNegative + }, + unfocusedBorderColor = if (isRegexValid) { + Variables.borderDefault + } else { + Variables.colorsIconNegative + }, + ), + modifier = Modifier + .fillMaxWidth() + .weight(1f), + shape = RoundedCornerShape(size = Variables.radiusMinimal), + trailingIcon = { + if (stringValue.isNotEmpty()) { + IconButton(onClick = { + onStringValueChange("") + }) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_close_black), + contentDescription = "Clear text", + tint = Variables.mainSubtle + ) + } + } + } + ) + + Spacer(modifier = Modifier.width(width = 4.dp)) + + Image( + painter = painterResource(id = R.drawable.ic_trash_can), + colorFilter = ColorFilter.tint(color =Variables.mainSubtle), + contentDescription = "image description", + contentScale = ContentScale.None, + modifier = Modifier.Companion + .padding(Variables.spacingMedium) + .clickable { + onTrashCanButtonClicked(true) + } + ) + } + Spacer(modifier = Modifier.height(height = 4.dp)) + + if (isRegexValid) { + Text( + text = "Enter value", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsMainSubtle, + ) + ) + } else { + Text( + text = "Value not valid", + style = TextStyle( + fontSize = Variables.TypefaceFontSize12, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsMainNegative, + ) + ) + } + + } + } +} + +@Composable +fun CustomTickToast(message: String) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(24.dp)) + .background(Color.DarkGray) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Tick Mark", + tint = Color.Green, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = message, + color = Color.White, + fontSize = 16.sp + ) + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/StringLengthFilterScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/StringLengthFilterScreen.kt new file mode 100644 index 0000000..13cbe0d --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/StringLengthFilterScreen.kt @@ -0,0 +1,317 @@ +package com.zebra.aidatacapturedemo.ui.view.filters + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.RangeSlider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.FilterType +import com.zebra.aidatacapturedemo.ui.view.Variables +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +@Composable +fun StringLengthFilterScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavHostController, + innerPadding: PaddingValues +) { + val uiState = viewModel.uiState.collectAsState().value + var stringLengthRange by remember { + mutableStateOf( + if (uiState.selectedFilterType == FilterType.OCR_FILTER) { + uiState.ocrFilterData.selectedStringLengthRange + } else { + uiState.barcodeFilterData.selectedStringLengthRange + } + ) + } + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + + viewModel.updateAppBarTitle(stringResource(R.string.ocr_filter_string_length_title)) + + uiState.toastMessage?.let { + viewModel.toast(it) + viewModel.updateToastMessage(message = null) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(innerPadding), + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(top = 16.dp, bottom = 16.dp) + .verticalScroll(rememberScrollState()) + ) { + InputRangeSliderFieldNew( + rangeSliderValue = stringLengthRange, + onRangeSliderValueChange = { stringLengthRange = it } + ) + } + + + // Bottom Action Buttons + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Spacer(modifier = Modifier.width(16.dp)) + // Cancel Button + Button( + onClick = { + viewModel.handleBackButton(navController) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.Black + ), + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .weight(1f) + .height(48.dp), + border = BorderStroke(1.dp, Variables.mainLight), + ) { + Text( + text = "Cancel", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.mainDefault, + textAlign = TextAlign.Center, + ) + ) + } + + // Save Button + Button( + onClick = { + viewModel.updateToastMessage("Save was successful.") + if (uiState.selectedFilterType == FilterType.OCR_FILTER) { + + val defaultOcrFilterData = uiState.ocrFilterData + defaultOcrFilterData.selectedStringLengthRange = stringLengthRange + viewModel.updateOcrFilterData(ocrFilterData = defaultOcrFilterData) + } else { + val defaultBarcodeFilterData = uiState.barcodeFilterData + defaultBarcodeFilterData.selectedStringLengthRange = stringLengthRange + viewModel.updateBarcodeFilterData(barcodeFilterData = defaultBarcodeFilterData) + } + viewModel.handleBackButton(navController) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Variables.mainPrimary, + contentColor = Variables.stateDefaultEnabled + ), + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .weight(1f) + .height(48.dp) + ) { + Text( + text = "Save", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.stateDefaultEnabled, + textAlign = TextAlign.Center, + ) + ) + } + Spacer(modifier = Modifier.width(16.dp)) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun InputRangeSliderFieldNew( + rangeSliderValue: ClosedFloatingPointRange, + onRangeSliderValueChange: (ClosedFloatingPointRange) -> Unit +) { + Column( + modifier = Modifier.background(color = Variables.colorsSurfaceCool) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) { + Spacer(modifier = Modifier.weight(1f)) + Text( + text = rangeSliderValue.start.toInt() + .toString(), + style = TextStyle( + fontSize = Variables.TypefaceFontSize14, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsMainSubtle, + textAlign = TextAlign.Center, + ), + modifier = Modifier.weight(1f) + ) + + Spacer(modifier = Modifier.weight(1f)) + RangeSlider( + value = rangeSliderValue, + onValueChange = { + onRangeSliderValueChange(it) + }, + valueRange = 2f..15f, + modifier = Modifier.weight(8f), + startThumb = { + CircularThumb() + }, + endThumb = { + CircularThumb() + }, +// track = { sliderState -> +// // Custom track implementation +// Box( +// modifier = Modifier +// .fillMaxWidth() +// .height(4.dp) // Set your desired track height here +// .background(Variables.mainLight) // Background color for the full track +// ) { +// // Determine the progress and apply the active track color to a sub-Box +//// val fraction = (sliderState.value.endInclusive - sliderState.value.start) / (sliderState.valueRange.endInclusive - sliderState.valueRange.start) +// val start = (sliderState.valueRange.start - sliderState.valueRange.start) / (sliderState.valueRange.endInclusive - sliderState.valueRange.start) +// +// Box( +// modifier = Modifier +// .fillMaxWidth(0.6f) // Fills the fraction of the track that is active +// .height(4.dp) // Same height +// .background(Variables.mainPrimary) // Color for the active portion +// // Need to manually place the active part at the correct start position. +// // A more robust solution might involve custom drawing with Canvas or a MeasurePolicy. +// // For simplicity here, the thumb still manages its own positioning relative to the *full* Box width. +// ) +// } +// } + + // Working #1 + track = { state -> + // Custom Track: Inactive (gray) + Active (primary) + Box( + Modifier + .fillMaxWidth() + .height(4.dp) + ) { + // Inactive part + Box( + Modifier + .fillMaxSize() + .background(Variables.mainSubtle) + ) + + // Active part (using RangeSliderState to calculate positioning) + SliderDefaults.Track( + rangeSliderState = state, + modifier = Modifier.height(4.dp), + colors = SliderDefaults.colors(activeTrackColor = Variables.mainPrimary) + ) + } + } + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = rangeSliderValue.endInclusive.toInt() + .toString(), + style = TextStyle( + fontSize = Variables.TypefaceFontSize14, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsMainSubtle, + textAlign = TextAlign.Center, + ), + modifier = Modifier.weight(1f) + ) + + Spacer(modifier = Modifier.weight(1f)) + } + Box( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = "Filter by character lengths", + style = TextStyle( + fontSize = 10.sp, + lineHeight = Variables.TypefaceLineHeight16, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsMainSubtle, + textAlign = TextAlign.Center + ) + ) + } + } +} + +@Composable +fun CircularThumb() { + Box( + modifier = Modifier + .size(16.dp) + .clip(shape = RoundedCornerShape(50)) + .background(color = Variables.mainPrimary), + ) +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt new file mode 100644 index 0000000..5f29243 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt @@ -0,0 +1,2127 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.viewmodel + +import android.annotation.SuppressLint +import android.app.Application +import android.content.Context +import android.content.res.AssetManager +import android.graphics.Bitmap +import android.graphics.Matrix +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraManager +import android.net.Uri +import android.util.Log +import android.util.Size +import android.view.ScaleGestureDetector +import android.widget.Toast +import androidx.camera.core.AspectRatio +import androidx.camera.core.Camera +import androidx.camera.core.CameraSelector +import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY +import androidx.camera.core.ImageCaptureException +import androidx.camera.core.ImageProxy +import androidx.camera.core.Preview +import androidx.camera.core.resolutionselector.AspectRatioStrategy +import androidx.camera.core.resolutionselector.ResolutionSelector +import androidx.camera.core.resolutionselector.ResolutionStrategy +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.getString +import androidx.core.net.toUri +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import androidx.navigation.NavController +import com.google.common.util.concurrent.ListenableFuture +import com.zebra.ai.vision.detector.AIVisionSDK +import com.zebra.ai.vision.detector.BBox +import com.zebra.ai.vision.detector.InvalidInputException +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.BarcodeFilterData +import com.zebra.aidatacapturedemo.data.BarcodeSettings +import com.zebra.aidatacapturedemo.data.BarcodeSymbology +import com.zebra.aidatacapturedemo.data.CommonSettings +import com.zebra.aidatacapturedemo.data.CustomerInfo +import com.zebra.aidatacapturedemo.data.FilterType +import com.zebra.aidatacapturedemo.data.ModuleData +import com.zebra.aidatacapturedemo.data.OcrBarcodeFindSettings +import com.zebra.aidatacapturedemo.data.OcrFilterData +import com.zebra.aidatacapturedemo.data.ProductData +import com.zebra.aidatacapturedemo.data.ProductInfo +import com.zebra.aidatacapturedemo.data.ProductRecognitionSettings +import com.zebra.aidatacapturedemo.data.ResultData +import com.zebra.aidatacapturedemo.data.RetailShelfSettings +import com.zebra.aidatacapturedemo.data.TextOcrSettings +import com.zebra.aidatacapturedemo.data.UsecaseState +import com.zebra.aidatacapturedemo.model.BarcodeAnalyzer +import com.zebra.aidatacapturedemo.model.FileUtils +import com.zebra.aidatacapturedemo.model.FileUtils.Companion.clearOcrBarcodeCaptureSessionPrefs +import com.zebra.aidatacapturedemo.model.FileUtils.Companion.databaseFile +import com.zebra.aidatacapturedemo.model.FileUtils.Companion.mCacheDir +import com.zebra.aidatacapturedemo.model.GenericEntityTrackerAnalyzer +import com.zebra.aidatacapturedemo.model.ProductEnrollmentRecognition +import com.zebra.aidatacapturedemo.model.RetailShelfAnalyzer +import com.zebra.aidatacapturedemo.model.TextOCRAnalyzer +import com.zebra.aidatacapturedemo.ui.view.Screen +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import java.io.BufferedReader +import java.io.File +import java.io.IOException +import java.io.InputStreamReader +import java.util.concurrent.Executor +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +private const val TAG = "AIDataCaptureDemoViewModel" +private const val CAMERA_TAG = "AIDCDemo_CameraProp" + +/** + * ViewModel class for the AIDataCaptureDemo + * Initializes AIVisionSDK, before using its components and all the models + */ +class AIDataCaptureDemoViewModel( + private val cacheDir: String, + private val context: Context, + private val assetManager: AssetManager +) : ViewModel() { + + // Used to set up a link between the Model and UI View. + private val _uiState = MutableStateFlow(AIDataCaptureDemoUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private var executor: Executor? = null + + private lateinit var cameraProviderFuture: ListenableFuture + private var cameraProvider: ProcessCameraProvider? = null + private var camera: Camera? = null + private var analysisUseCase: ImageAnalysis? = null + private lateinit var imageCaptureResolutionSelector: ResolutionSelector + private var imageCapture: ImageCapture? = null + + private var ocrAnalyzer: TextOCRAnalyzer? = null + private var retailShelfAnalyzer: RetailShelfAnalyzer? = null + private var barcodeAnalyzer: BarcodeAnalyzer? = null + private var genericEntityTrackerAnalyzer : GenericEntityTrackerAnalyzer? = null + private var productEnrollmentRecognition: ProductEnrollmentRecognition? = null + + companion object { + fun factory() = viewModelFactory { + initializer { + val application = + this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application + AIDataCaptureDemoViewModel( + application.filesDir.absolutePath, + application as Context, + application.assets + ) + } + } + } + + init { + executor = Dispatchers.Default.asExecutor() + + val isInitDone = AIVisionSDK.getInstance(context).init() + Log.i(TAG, "AI Vision SDK Init ret = $isInitDone") + + // Get the SDK version + val sdkVersion = AIVisionSDK.getInstance(context).sdkVersion + Log.i(TAG, "AI Vision SDK Version = $sdkVersion") + + } + + /** + * This function is used to initialize the model based on the selected index + */ + fun initModel() { + CoroutineScope(executor!!.asCoroutineDispatcher()).launch { + + if(genericEntityTrackerAnalyzer == null) { + genericEntityTrackerAnalyzer = + GenericEntityTrackerAnalyzer(uiState, viewModel = this@AIDataCaptureDemoViewModel) + } + + when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value -> { + barcodeAnalyzer = BarcodeAnalyzer( + uiState = uiState, + viewModel = this@AIDataCaptureDemoViewModel + ) + barcodeAnalyzer?.initialize() + } + + UsecaseState.BarcodeMap.value -> { + barcodeAnalyzer = BarcodeAnalyzer( + uiState = uiState, + viewModel = this@AIDataCaptureDemoViewModel + ) + barcodeAnalyzer?.initialize() + _uiState.update { currentState -> + currentState.copy( + isCaptureOrLiveEnabled = 0 // Default to Capture for Barcode Map + ) + } + } + + UsecaseState.Retail.value -> { + retailShelfAnalyzer = RetailShelfAnalyzer( + uiState = uiState, + viewModel = this@AIDataCaptureDemoViewModel, + cacheDir = context.filesDir.absolutePath + ) + retailShelfAnalyzer?.initialize() + } + + UsecaseState.Product.value -> { + productEnrollmentRecognition = + ProductEnrollmentRecognition( + uiState = uiState, + viewModel = this@AIDataCaptureDemoViewModel, + cacheDir = context.filesDir.absolutePath + ) + productEnrollmentRecognition?.initialize() + } + + UsecaseState.OCRBarcodeFind.value->{ + if(uiState.value.isOCRModelEnabled) { + ocrAnalyzer = TextOCRAnalyzer( + uiState = uiState, + viewModel = this@AIDataCaptureDemoViewModel + ) + ocrAnalyzer?.initialize() + } + if(uiState.value.isBarcodeModelEnabled) { + barcodeAnalyzer = BarcodeAnalyzer( + uiState = uiState, + viewModel = this@AIDataCaptureDemoViewModel + ) + barcodeAnalyzer?.initialize() + } + } + UsecaseState.OCR.value -> { + ocrAnalyzer = TextOCRAnalyzer( + uiState = uiState, + viewModel = this@AIDataCaptureDemoViewModel + ) + ocrAnalyzer?.initialize() + } + } + } + } + + /** + * This function is used to de-initialize the model based on the selected value + */ + fun deinitModel() { + when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + barcodeAnalyzer?.deinitialize() + barcodeAnalyzer = null + } + + UsecaseState.Retail.value -> { + retailShelfAnalyzer?.deinitialize() + retailShelfAnalyzer = null + } + + UsecaseState.Product.value -> { + productEnrollmentRecognition?.deinitialize() + productEnrollmentRecognition = null + } + + UsecaseState.OCRBarcodeFind.value -> { + ocrAnalyzer?.deinitialize() + ocrAnalyzer = null + + barcodeAnalyzer?.deinitialize() + barcodeAnalyzer = null + } + UsecaseState.OCR.value -> { + ocrAnalyzer?.deinitialize() + ocrAnalyzer = null + } + } + genericEntityTrackerAnalyzer = null + } + + /** + * This function is used to start processing + */ + fun startProcessing() { + if (uiState.value.usecaseSelected == UsecaseState.Product.value ) { + productEnrollmentRecognition?.startAnalyzing() + } + } + + /** + * This function is used to stop processing + */ + fun stopProcessing() { + if (uiState.value.usecaseSelected == UsecaseState.Product.value ) { + productEnrollmentRecognition?.stopAnalyzing() + } + } + + @SuppressLint("ClickableViewAccessibility", "RestrictedApi") + public fun setupCameraController( + previewView: PreviewView, + analysisUseCaseCameraResolution: Size, + lifecycleOwner: LifecycleOwner, + activityLifecycle: Lifecycle + ) { + cameraProviderFuture = ProcessCameraProvider.getInstance(context) + cameraProviderFuture.addListener({ + try { + cameraProvider = cameraProviderFuture.get() + + printCameraSupportedResolution() + val isBackCameraAvailable = hasBackCamera(cameraProvider = cameraProvider!!) + Log.d(TAG, "isBackCameraAvailable = $isBackCameraAvailable") + + val selectedCameraLensFacing = if (isBackCameraAvailable) { + CameraSelector.LENS_FACING_BACK + } else { + CameraSelector.LENS_FACING_FRONT + } + + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(selectedCameraLensFacing) + .build() + + // PREVIEW USE CASE + val previewUsecaseResolutionSelector = ResolutionSelector.Builder() + .setAspectRatioStrategy( + AspectRatioStrategy( + AspectRatio.RATIO_16_9, + AspectRatioStrategy.FALLBACK_RULE_NONE + ) + ) + .build() + + val previewUsecase = + Preview.Builder().setResolutionSelector(previewUsecaseResolutionSelector) + .build() + + // ANALYSIS USE CASE + val analysisUsecaseResolutionSelector = ResolutionSelector.Builder() + .setAspectRatioStrategy( + AspectRatioStrategy( + AspectRatio.RATIO_16_9, + AspectRatioStrategy.FALLBACK_RULE_NONE + ) + ) + .setResolutionStrategy( + ResolutionStrategy( + analysisUseCaseCameraResolution, + ResolutionStrategy.FALLBACK_RULE_NONE + ) + ).build() + + analysisUseCase = ImageAnalysis.Builder() + .setResolutionSelector(analysisUsecaseResolutionSelector) + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + + // Set the appropriate analyzer based on the selectedUsecase + setAnalyzer(activityLifecycle) + cameraProvider?.unbindAll() + + // Bind an additional Capture Use Case only for Product Recognition UsecaseState + camera = if ((uiState.value.usecaseSelected == UsecaseState.Product.value) || + (uiState.value.usecaseSelected == UsecaseState.BarcodeMap.value) || + ((uiState.value.usecaseSelected == UsecaseState.OCRBarcodeFind.value) && (uiState.value.isCaptureOrLiveEnabled == 0))){ + // HIGH-RES CAPTURE CASE + imageCaptureResolutionSelector = ResolutionSelector.Builder() + .setAspectRatioStrategy( + AspectRatioStrategy( + AspectRatio.RATIO_16_9, + AspectRatioStrategy.FALLBACK_RULE_NONE + ) + ) + .build() + + imageCapture = ImageCapture.Builder() + .setResolutionSelector(imageCaptureResolutionSelector) + .setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY).build() + + cameraProvider?.bindToLifecycle( + lifecycleOwner, + cameraSelector, + previewUsecase, + imageCapture, + analysisUseCase + ) + } else { + cameraProvider?.bindToLifecycle( + lifecycleOwner, + cameraSelector, + previewUsecase, + analysisUseCase + ) + } + previewUsecase.setSurfaceProvider(previewView.surfaceProvider) + + updateCameraReady(true) + + val previewUseCaseSize = previewUsecase.attachedSurfaceResolution ?: Size(0, 0) + Log.d(TAG, "Attached PreviewUsecase Resolution = $previewUseCaseSize") + + val analysisUseCaseSize = analysisUseCase?.attachedSurfaceResolution ?: Size(0, 0) + Log.d(TAG, "Attached analysisUsecase Resolution = $analysisUseCaseSize") + + val imageCaptureUseCaseSize = imageCapture?.attachedSurfaceResolution ?: Size(0, 0) + Log.d(TAG, "Attached imageCaptureUsecase Resolution = $imageCaptureUseCaseSize") + + + //Pinch to Zoom handling + val scaleGestureDetector = ScaleGestureDetector( + context, + object : ScaleGestureDetector.SimpleOnScaleGestureListener() { + override fun onScale(detector: ScaleGestureDetector): Boolean { + val cameraControl = camera?.cameraControl // Get CameraControl instance + val cameraInfo = camera?.cameraInfo // Get CameraInfo instance + + if (cameraControl != null && cameraInfo != null) { + val currentZoomRatio = cameraInfo.zoomState.value?.zoomRatio ?: 1.0f + val newZoomRatio = currentZoomRatio * detector.scaleFactor + + // Clamp the new zoom ratio within the camera's supported range + val minZoomRatio = cameraInfo.zoomState.value?.minZoomRatio ?: 1.0f + val maxZoomRatio = cameraInfo.zoomState.value?.maxZoomRatio ?: 1.0f + val clampedZoomRatio = + newZoomRatio.coerceIn(minZoomRatio, maxZoomRatio) + + setZoom(clampedZoomRatio) + } + return true + } + }) + + previewView.setOnTouchListener { _, event -> + scaleGestureDetector.onTouchEvent(event) + true // Indicate that the event was consumed + } + + } catch (e: IllegalArgumentException) { + Log.e(TAG, "IllegalArgumentException while setting up the camera. Exception = ${e.message}") + e.message?.let { + if (it.contains("May be attempting to bind too many use cases") || + it.contains("No available output size is found") + ) { + val errorMessage = getString(context, R.string.instruction_6) + toast(toastString = errorMessage) + updateCameraErrorMessage(errorMessage = errorMessage) + } + } + } catch (e: Exception) { + Log.e(TAG, " Exception while setting up the camera : ${e.message}") + } + }, ContextCompat.getMainExecutor(context)) + } + + private fun printCameraSupportedResolution() { + val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + + Log.d(CAMERA_TAG, "Printing Camera's supported Resolutions:") + cameraManager.cameraIdList.forEach { cameraId -> + cameraId?.let { it -> + + + Log.d(CAMERA_TAG, "cameraId = $it") + val characteristics = cameraManager.getCameraCharacteristics(it) + + val facing = characteristics.get(CameraCharacteristics.LENS_FACING) + + if (facing != null) { + when (facing) { + CameraCharacteristics.LENS_FACING_FRONT -> { + Log.d(CAMERA_TAG, "Camera facing = front-facing camera") + } + + CameraCharacteristics.LENS_FACING_BACK -> { + Log.d(CAMERA_TAG, "Camera facing = back-facing camera") + } + + CameraCharacteristics.LENS_FACING_EXTERNAL -> { + Log.d(CAMERA_TAG, "Camera facing = external camera") + } + } + } + val configMap = + characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) + + android.graphics.ImageFormat.PRIVATE + configMap?.outputFormats?.forEach { format -> + + val formatName = when (format) { + 1144402265 -> "DEPTH16" + 1768253795 -> "DEPTH_JPEG" + 257 -> "DEPTH_POINT_CLOUD" + 42 -> "FLEX_RGBA_8888" + 41 -> "FLEX_RGB_888" + 1212500294 -> "HEIC" + 4102 -> "HEIC_ULTRAHDR" + 256 -> "JPEG" + 4101 -> "JPEG_R" + 16 -> "NV16" + 17 -> "NV21" + 34 -> "PRIVATE" + 37 -> "RAW10" + 38 -> "RAW12" + 36 -> "RAW_PRIVATE" + 32 -> "RAW_SENSOR" + 4 -> "RGB_565" + 0 -> "UNKNOWN" + 538982489 -> "Y8" + 54 -> "YCBCR_P010" + 60 -> "YCBCR_P210" + 35 -> "YUV_420_888" + 39 -> "YUV_422_888" + 40 -> "YUV_444_888" + 20 -> "YUY2" + 842094169 -> "YV12" + else -> "N/A" + } + Log.d(CAMERA_TAG, "Format = $formatName") + Log.d(CAMERA_TAG, "Supported Preview Size:") + val previewSizes: Array? = configMap?.getOutputSizes(format) + previewSizes?.forEach { size -> + val aspectRatio = size.width.toFloat() / size.height.toFloat() + val mp = (size.width.toFloat() * size.height.toFloat()) / 1000000 + Log.d( + CAMERA_TAG, + "${size.width}x${size.height}, Ratio : ${aspectRatio}, MP : $mp" + ) + } + + Log.d(CAMERA_TAG, "Supported HigRes Size:") + val highResSizes: Array? = configMap?.getHighResolutionOutputSizes(format) + highResSizes?.forEach { size -> + val aspectRatio = size.width.toFloat() / size.height.toFloat() + val mp = (size.width.toFloat() * size.height.toFloat()) / 1000000 + Log.d( + CAMERA_TAG, + "${size.width}x${size.height}, Ratio : ${aspectRatio}, MP : $mp" + ) + } + } + + // Get supported high-resolution capture sizes + val captureSizes: Array? = + configMap?.getHighResolutionOutputSizes(android.graphics.ImageFormat.JPEG) + Log.d(CAMERA_TAG, "Supported High-Resolution Capture Sizes:") + captureSizes?.forEach { size -> + Log.d(CAMERA_TAG, "Capture resolution: ${size.width}x${size.height}") + } + } + } + } + + private fun hasBackCamera(cameraProvider: ProcessCameraProvider): Boolean { + return try { + cameraProvider.hasCamera(DEFAULT_BACK_CAMERA) + } catch (e: Exception) { + // A camera may not be available for the requested selector + false + } + } + + /** + * Turn on torch + */ + fun enableTorch(enabled: Boolean) { + if (camera?.cameraInfo?.hasFlashUnit() == true) { + camera?.cameraControl?.enableTorch(enabled) + } + } + + /** + * Set Zoom Value + */ + fun setZoom(zoomValue: Float) { + _uiState.update { currentState -> + currentState.copy( + zoomLevel = zoomValue + ) + } + camera?.cameraControl?.setZoomRatio(uiState.value.zoomLevel) + } + + /** + * Update the selected usecase + */ + fun updateSelectedUsecase(usecase: String) { + _uiState.update { currentState -> + currentState.copy( + usecaseSelected = usecase + ) + } + } + + /** + * Update the selected processor + */ + fun updateSelectedProcessor(index: Int) { + val updatedSelectedProcessorIndex = when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + val currentProcessorSelectedIndex = _uiState.value.barcodeSettings.commonSettings + currentProcessorSelectedIndex.copy(processorSelectedIndex = index) + } + + UsecaseState.Retail.value -> { + val currentProcessorSelectedIndex = + _uiState.value.retailShelfSettings.commonSettings + currentProcessorSelectedIndex.copy(processorSelectedIndex = index) + } + + UsecaseState.Product.value -> { + val currentProcessorSelectedIndex = + _uiState.value.productRecognitionSettings.commonSettings + currentProcessorSelectedIndex.copy(processorSelectedIndex = index) + } + + UsecaseState.OCRBarcodeFind.value -> { + val currentProcessorSelectedIndex = _uiState.value.ocrBarcodeFindSettings.commonSettings + currentProcessorSelectedIndex.copy(processorSelectedIndex = index) + } + + UsecaseState.OCR.value -> { + val currentProcessorSelectedIndex = _uiState.value.textOCRSettings.commonSettings + currentProcessorSelectedIndex.copy(processorSelectedIndex = index) + } + + else -> { + 0 + } + } + when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> _uiState.value.barcodeSettings.commonSettings = + updatedSelectedProcessorIndex as CommonSettings + + UsecaseState.Retail.value -> _uiState.value.retailShelfSettings.commonSettings = + updatedSelectedProcessorIndex as CommonSettings + + UsecaseState.Product.value -> _uiState.value.productRecognitionSettings.commonSettings = + updatedSelectedProcessorIndex as CommonSettings + + UsecaseState.OCRBarcodeFind.value -> _uiState.value.ocrBarcodeFindSettings.commonSettings = + updatedSelectedProcessorIndex as CommonSettings + + UsecaseState.OCR.value -> _uiState.value.textOCRSettings.commonSettings = + updatedSelectedProcessorIndex as CommonSettings + } + if ((uiState.value.usecaseSelected == UsecaseState.OCRBarcodeFind.value) || (uiState.value.usecaseSelected == UsecaseState.OCR.value)) { + updateSelectedDimensions(0) + } + } + + /** + * Update the selected dimensions + */ + fun updateSelectedDimensions(index: Int) { + var dimension = when (index) { + 0 -> 640 + 1 -> 1280 + 2 -> 1600 + 3 -> 2560 + else -> throw InvalidInputException( + "Invalid dimension selection ${index}" + ) + } + + val updatedInputSize = when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + val currentInputSizeSelected = _uiState.value.barcodeSettings.commonSettings + currentInputSizeSelected.copy(inputSizeSelected = dimension) + } + + UsecaseState.Retail.value -> { + val currentInputSizeSelected = _uiState.value.retailShelfSettings.commonSettings + currentInputSizeSelected.copy(inputSizeSelected = dimension) + } + + UsecaseState.Product.value -> { + val currentInputSizeSelected = + _uiState.value.productRecognitionSettings.commonSettings + currentInputSizeSelected.copy(inputSizeSelected = dimension) + } + + UsecaseState.OCRBarcodeFind.value -> { + val currentInputSizeSelected = _uiState.value.ocrBarcodeFindSettings.commonSettings + if (currentInputSizeSelected.processorSelectedIndex == 2) { + dimension = 640 + } + currentInputSizeSelected.copy(inputSizeSelected = dimension) + } + + UsecaseState.OCR.value -> { + val currentInputSizeSelected = _uiState.value.textOCRSettings.commonSettings + if (currentInputSizeSelected.processorSelectedIndex == 2) { + dimension = 640 + } + currentInputSizeSelected.copy(inputSizeSelected = dimension) + } + + else -> { + 1280 + } + } + when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> _uiState.value.barcodeSettings.commonSettings = + updatedInputSize as CommonSettings + + UsecaseState.Retail.value -> _uiState.value.retailShelfSettings.commonSettings = + updatedInputSize as CommonSettings + + UsecaseState.Product.value -> _uiState.value.productRecognitionSettings.commonSettings = + updatedInputSize as CommonSettings + + UsecaseState.OCRBarcodeFind.value -> _uiState.value.ocrBarcodeFindSettings.commonSettings = + updatedInputSize as CommonSettings + + UsecaseState.OCR.value -> _uiState.value.textOCRSettings.commonSettings = + updatedInputSize as CommonSettings + } + } + + fun updateSelectedResolution(index: Int) { + val updatedResolution = when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + val currentResolutionSelectedIndex = _uiState.value.barcodeSettings.commonSettings + currentResolutionSelectedIndex.copy(resolutionSelectedIndex = index) + } + + UsecaseState.Retail.value -> { + val currentResolutionSelectedIndex = + _uiState.value.retailShelfSettings.commonSettings + currentResolutionSelectedIndex.copy(resolutionSelectedIndex = index) + } + + UsecaseState.Product.value -> { + val currentResolutionSelectedIndex = + _uiState.value.productRecognitionSettings.commonSettings + currentResolutionSelectedIndex.copy(resolutionSelectedIndex = index) + } + + UsecaseState.OCRBarcodeFind.value -> { + val currentResolutionSelectedIndex = _uiState.value.ocrBarcodeFindSettings.commonSettings + currentResolutionSelectedIndex.copy(resolutionSelectedIndex = index) + } + + UsecaseState.OCR.value -> { + val currentResolutionSelectedIndex = _uiState.value.textOCRSettings.commonSettings + currentResolutionSelectedIndex.copy(resolutionSelectedIndex = index) + } + + else -> { + 1 + } + } + when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> _uiState.value.barcodeSettings.commonSettings = + updatedResolution as CommonSettings + + UsecaseState.Retail.value -> _uiState.value.retailShelfSettings.commonSettings = + updatedResolution as CommonSettings + + UsecaseState.Product.value -> _uiState.value.productRecognitionSettings.commonSettings = + updatedResolution as CommonSettings + + UsecaseState.OCRBarcodeFind.value -> _uiState.value.ocrBarcodeFindSettings.commonSettings = + updatedResolution as CommonSettings + + UsecaseState.OCR.value -> _uiState.value.textOCRSettings.commonSettings = + updatedResolution as CommonSettings + } + } + + fun getSelectedResolution(): Int? { + val currentResolutionSelectedIndex = when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + _uiState.value.barcodeSettings.commonSettings.resolutionSelectedIndex + } + + UsecaseState.Retail.value -> { + _uiState.value.retailShelfSettings.commonSettings.resolutionSelectedIndex + } + + UsecaseState.Product.value -> { + _uiState.value.productRecognitionSettings.commonSettings.resolutionSelectedIndex + } + + UsecaseState.OCRBarcodeFind.value -> { + _uiState.value.ocrBarcodeFindSettings.commonSettings.resolutionSelectedIndex + } + + UsecaseState.OCR.value -> { + _uiState.value.textOCRSettings.commonSettings.resolutionSelectedIndex + } + + else -> { + null + } + } + return currentResolutionSelectedIndex + } + + fun getProcessorSelectedIndex(): Int? { + val currentProcessorSelectedIndex = when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + _uiState.value.barcodeSettings.commonSettings.processorSelectedIndex + } + + UsecaseState.Retail.value -> { + _uiState.value.retailShelfSettings.commonSettings.processorSelectedIndex + } + + UsecaseState.Product.value -> { + _uiState.value.productRecognitionSettings.commonSettings.processorSelectedIndex + } + + UsecaseState.OCRBarcodeFind.value -> { + _uiState.value.ocrBarcodeFindSettings.commonSettings.processorSelectedIndex + } + + UsecaseState.OCR.value -> { + _uiState.value.textOCRSettings.commonSettings.processorSelectedIndex + } + + else -> { + null + } + } + return currentProcessorSelectedIndex + } + + fun getInputSizeSelected(): Int? { + val currentInputSizeSelected = when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + _uiState.value.barcodeSettings.commonSettings.inputSizeSelected + } + + UsecaseState.Retail.value -> { + _uiState.value.retailShelfSettings.commonSettings.inputSizeSelected + } + + UsecaseState.Product.value -> { + _uiState.value.productRecognitionSettings.commonSettings.inputSizeSelected + } + + UsecaseState.OCRBarcodeFind.value -> { + _uiState.value.ocrBarcodeFindSettings.commonSettings.inputSizeSelected + } + + UsecaseState.OCR.value -> { + _uiState.value.textOCRSettings.commonSettings.inputSizeSelected + } + + else -> { + null + } + } + return currentInputSizeSelected + } + +// fun getOCRFilterTypeData() : OCRFilterData { +// val ocrFilterTypeData = if (uiState.value.usecaseSelected == UsecaseState.OCR.value) { +// OCRFilterData(ocrFilterType = OCRFilterType.SHOW_ALL) +// } else { +// when (uiState.value.selectedOcrFilterType) { +// OCRFilterType.SHOW_ALL -> { +// OCRFilterData(ocrFilterType = OCRFilterType.SHOW_ALL) +// } +// +// OCRFilterType.NUMERIC_CHARACTERS_ONLY -> { +// OCRFilterData( +// ocrFilterType = OCRFilterType.NUMERIC_CHARACTERS_ONLY, +// charLengthMin = uiState.value.selectedNumericCharSliderValues.start.toInt(), +// charLengthMax = uiState.value.selectedNumericCharSliderValues.endInclusive.toInt() +// ) +// } +// +// OCRFilterType.ALPHA_CHARACTERS_ONLY -> { +// OCRFilterData( +// ocrFilterType = OCRFilterType.ALPHA_CHARACTERS_ONLY, +// charLengthMin = uiState.value.selectedAlphaCharSliderValues.start.toInt(), +// charLengthMax = uiState.value.selectedAlphaCharSliderValues.endInclusive.toInt() +// ) +// } +// +// OCRFilterType.ALPHA_NUMERIC_CHARACTERS_ONLY -> { +// OCRFilterData( +// ocrFilterType = OCRFilterType.ALPHA_NUMERIC_CHARACTERS_ONLY, +// charLengthMin = uiState.value.selectedAlphaNumericCharSliderValues.start.toInt(), +// charLengthMax = uiState.value.selectedAlphaNumericCharSliderValues.endInclusive.toInt() +// ) +// } +// +// OCRFilterType.EXACT_MATCH -> { +// OCRFilterData( +// ocrFilterType = OCRFilterType.EXACT_MATCH, +// exactMatchString = uiState.value.selectedExactMatchString +// ) +// } +// } +// } +// return ocrFilterTypeData +// } + + /** + * Update the barcode symbologies + */ + fun updateBarcodeSymbology(name: String, enabled: Boolean) { + var currentSymbology = BarcodeSymbology() + if(_uiState.value.usecaseSelected == UsecaseState.OCRBarcodeFind.value){ + currentSymbology = _uiState.value.ocrBarcodeFindSettings.barcodeSymbology + } else { + currentSymbology = _uiState.value.barcodeSettings.barcodeSymbology + } + val updatedSymbology = when (name) { + getString(context, R.string.australian_postal) -> currentSymbology.copy( + australian_postal = enabled + ) + + + getString(context, R.string.aztec) -> { + currentSymbology.copy( + aztec = enabled + ) + } + + getString(context, R.string.canadian_postal) -> { + currentSymbology.copy( + canadian_postal = enabled + ) + } + + getString(context, R.string.chinese_2of5) -> { + currentSymbology.copy( + chinese_2of5 = enabled + ) + } + + getString(context, R.string.codabar) -> { + currentSymbology.copy( + codabar = enabled + ) + } + + getString(context, R.string.code11) -> { + currentSymbology.copy( + code11 = enabled + ) + + } + + getString(context, R.string.code39) -> { + + currentSymbology.copy( + code39 = enabled + ) + + } + + getString(context, R.string.code93) -> { + + currentSymbology.copy( + code93 = enabled + ) + + } + + getString(context, R.string.code128) -> { + + currentSymbology.copy( + code128 = enabled + ) + + } + + getString(context, R.string.composite_ab) -> { + + currentSymbology.copy( + composite_ab = enabled + ) + + } + + getString(context, R.string.composite_c) -> { + + currentSymbology.copy( + composite_c = enabled + ) + + } + + getString(context, R.string.d2of5) -> { + + currentSymbology.copy( + d2of5 = enabled + ) + + } + + getString(context, R.string.datamatrix) -> { + + currentSymbology.copy( + datamatrix = enabled + ) + + } + + getString(context, R.string.dotcode) -> { + + currentSymbology.copy( + dotcode = enabled + ) + + } + + getString(context, R.string.dutch_postal) -> { + + currentSymbology.copy( + dutch_postal = enabled + ) + + } + + getString(context, R.string.ean_8) -> { + + currentSymbology.copy( + ean_8 = enabled + ) + + } + + getString(context, R.string.ean_13) -> { + + currentSymbology.copy( + ean_13 = enabled + ) + + } + + getString(context, R.string.finnish_postal_4s) -> { + + currentSymbology.copy( + finnish_postal_4s = enabled + ) + + } + + getString(context, R.string.grid_matrix) -> { + + currentSymbology.copy( + grid_matrix = enabled + ) + + } + + getString(context, R.string.gs1_databar) -> { + + currentSymbology.copy( + gs1_databar = enabled + ) + + } + + getString(context, R.string.gs1_databar_expanded) -> { + + currentSymbology.copy( + gs1_databar_expanded = enabled + ) + + } + + getString(context, R.string.gs1_databar_lim) -> { + + currentSymbology.copy( + gs1_databar_lim = enabled + ) + + } + + getString(context, R.string.gs1_datamatrix) -> { + + currentSymbology.copy( + gs1_datamatrix = enabled + ) + } + + getString(context, R.string.gs1_qrcode) -> { + + currentSymbology.copy( + gs1_qrcode = enabled + ) + + } + + getString(context, R.string.hanxin) -> { + + currentSymbology.copy( + hanxin = enabled + ) + + } + + getString(context, R.string.i2of5) -> { + + currentSymbology.copy( + i2of5 = enabled + ) + + } + + getString(context, R.string.japanese_postal) -> { + + currentSymbology.copy( + japanese_postal = enabled + ) + + } + + getString(context, R.string.korean_3of5) -> { + + currentSymbology.copy( + korean_3of5 = enabled + ) + + } + + getString(context, R.string.mailmark) -> { + + currentSymbology.copy( + mailmark = enabled + ) + } + + getString(context, R.string.matrix_2of5) -> { + + currentSymbology.copy( + matrix_2of5 = enabled + ) + + } + + getString(context, R.string.maxicode) -> { + + currentSymbology.copy( + maxicode = enabled + ) + + } + + getString(context, R.string.micropdf) -> { + + currentSymbology.copy( + micropdf = enabled + ) + + } + + getString(context, R.string.microqr) -> { + + currentSymbology.copy( + microqr = enabled + ) + + } + + getString(context, R.string.msi) -> { + + currentSymbology.copy( + msi = enabled + ) + + } + + getString(context, R.string.pdf417) -> { + + currentSymbology.copy( + pdf417 = enabled + ) + + } + + getString(context, R.string.qrcode) -> { + + currentSymbology.copy( + qrcode = enabled + ) + + } + + getString(context, R.string.tlc39) -> { + + currentSymbology.copy( + tlc39 = enabled + ) + + } + + getString(context, R.string.trioptic39) -> { + + currentSymbology.copy( + trioptic39 = enabled + ) + + } + + getString(context, R.string.uk_postal) -> { + + currentSymbology.copy( + uk_postal = enabled + ) + + } + + getString(context, R.string.upc_a) -> { + + currentSymbology.copy( + upc_a = enabled + ) + + } + + getString(context, R.string.upce0) -> { + + currentSymbology.copy( + upce0 = enabled + ) + + } + + getString(context, R.string.upce1) -> { + + currentSymbology.copy( + upce1 = enabled + ) + } + + getString(context, R.string.usplanet) -> { + + currentSymbology.copy( + usplanet = enabled + ) + + } + + getString(context, R.string.uspostnet) -> { + + currentSymbology.copy( + uspostnet = enabled + ) + + } + + getString(context, R.string.us4state) -> { + + currentSymbology.copy( + us4state = enabled + ) + + } + + getString(context, R.string.us4state_fics) -> { + + currentSymbology.copy( + us4state_fics = enabled + ) + + } + + else -> { + currentSymbology + } + } + if(_uiState.value.usecaseSelected == UsecaseState.OCRBarcodeFind.value){ + _uiState.value.ocrBarcodeFindSettings.barcodeSymbology = updatedSymbology + } else { + _uiState.value.barcodeSettings.barcodeSymbology = updatedSymbology + } + } + + fun updateFeedback(name: String, enabled: Boolean) { + if(_uiState.value.usecaseSelected == UsecaseState.OCRBarcodeFind.value) { + var currentFeedback = _uiState.value.ocrBarcodeFindSettings.feedbackSettings + + val updatedFeedback = when (name) { + getString(context, R.string.audio) -> currentFeedback.copy( + audioBeep = enabled + ) + getString(context, R.string.haptic) -> { + currentFeedback.copy( + vibration = enabled + ) + } + + getString(context, R.string.show_all_detected_barcodes) -> { + currentFeedback.copy( + showDetectedBarcode = enabled + ) + } + else -> { + currentFeedback + } + } + _uiState.value.ocrBarcodeFindSettings.feedbackSettings = updatedFeedback + } + } + + fun updateBarcodeModelEnabled(enabled: Boolean) { + _uiState.update { currentState -> + currentState.copy( + isBarcodeModelEnabled = enabled + ) + } + } + fun updateCaptureOrLiveEnabled(mode: Int) { + _uiState.update { currentState -> + currentState.copy( + isCaptureOrLiveEnabled = mode + ) + } + } + fun updateOCRModelEnabled(enabled: Boolean) { + _uiState.update { currentState -> + currentState.copy( + isOCRModelEnabled = enabled + ) + } + } + fun updateAllBarcodeOCRCaptureFilter(mode: Int) { + _uiState.update { currentState -> + currentState.copy( + allBarcodeOCRCaptureFilter = mode + ) + } + } + /** + * Update the current bitmap used for processing by the models + */ + fun updateBitmap(bitmap: Bitmap, rotation: Int) { + val matrix: Matrix = Matrix() + matrix.postRotate(rotation.toFloat()) + val rotatedBitmap = + Bitmap.createBitmap( + bitmap, + 0, + 0, + bitmap.width, + bitmap.height, + matrix, + true + ) + + _uiState.update { currentState -> + currentState.copy( + currentBitmap = rotatedBitmap + ) + } + } + + suspend fun takePicture(): Bitmap = suspendCancellableCoroutine { continuation -> + executor?.let { cameraExecutor -> + imageCapture!!.takePicture( + cameraExecutor, + object : ImageCapture.OnImageCapturedCallback() { + override fun onCaptureSuccess(image: ImageProxy) { + val highResBitmap: Bitmap = rotateBitmapIfNeeded(imageProxy = image)!! + image.close() + continuation.resume(highResBitmap) + } + + override fun onError(exception: ImageCaptureException) { + continuation.resumeWithException(exception) + } + }) + } + } + + fun rotateBitmapIfNeeded(imageProxy: ImageProxy): Bitmap? { + try { + val bitmap = imageProxy.toBitmap() + val rotationDegrees = imageProxy.imageInfo.rotationDegrees + return rotateBitmap(bitmap, rotationDegrees) + } catch (e: Exception) { + Log.e(TAG, "Error converting image to bitmap: " + e.message) + return null + } + } + + private fun rotateBitmap(bitmap: Bitmap?, degrees: Int): Bitmap? { + if (degrees == 0 || bitmap == null) return bitmap + + try { + val matrix = Matrix() + matrix.postRotate(degrees.toFloat()) + return Bitmap.createBitmap( + bitmap, + 0, + 0, + bitmap.getWidth(), + bitmap.getHeight(), + matrix, + true + ) + } catch (e: Exception) { + Log.e(TAG, "Error rotating bitmap: " + e.message) + return bitmap + } + } + + private fun setAnalyzer(activityLifecycle: Lifecycle) { + when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value -> { + barcodeAnalyzer?.let { + genericEntityTrackerAnalyzer?.addDecoder(it.getDetector()!!) + val analyzer = genericEntityTrackerAnalyzer?.setupEntityTrackerAnalyzer(activityLifecycle) + analysisUseCase?.setAnalyzer(executor!!, analyzer!! as ImageAnalysis.Analyzer) + } + } + + UsecaseState.Retail.value -> { + retailShelfAnalyzer?.let { + genericEntityTrackerAnalyzer?.addDecoder(it.getDetector()!!) + val analyzer = genericEntityTrackerAnalyzer?.setupEntityTrackerAnalyzer(activityLifecycle) + analysisUseCase?.setAnalyzer(executor!!, analyzer!! as ImageAnalysis.Analyzer) + } + } + + UsecaseState.Product.value -> { + productEnrollmentRecognition?.let { + analysisUseCase?.setAnalyzer(executor!!, it) + } + } + + UsecaseState.OCRBarcodeFind.value -> { + // Setup Analyzer only if Live mode is enabled. + // In case of Captured Image mode, Barcode and OCR decoders process captured image instead of using the GenericEntityTrackerAnalyzer + if(uiState.value.isCaptureOrLiveEnabled == 1) { + ocrAnalyzer?.let { + genericEntityTrackerAnalyzer?.addDecoder(it.getDetector()!!) + } + barcodeAnalyzer?.let { + genericEntityTrackerAnalyzer?.addDecoder(it.getDetector()!!) + } + val analyzer = + genericEntityTrackerAnalyzer?.setupEntityTrackerAnalyzer(activityLifecycle) + analysisUseCase?.setAnalyzer(executor!!, analyzer!! as ImageAnalysis.Analyzer) + } + } + UsecaseState.OCR.value -> { + ocrAnalyzer?.let { + genericEntityTrackerAnalyzer?.addDecoder(it.getDetector()!!) + val analyzer = genericEntityTrackerAnalyzer?.setupEntityTrackerAnalyzer(activityLifecycle) + analysisUseCase?.setAnalyzer(executor!!, analyzer!! as ImageAnalysis.Analyzer) + } + } + } + } + + fun updateAppBarTitle(title: String) { + _uiState.update { currentState -> + currentState.copy( + appBarTitle = title + ) + } + } + + fun updateOCRTextFieldValues(name: String, value: String) { + val advancedOCRSetting = _uiState.value.textOCRSettings.advancedOCRSetting + val updatedOCRSetting = + when (name) { + getString(context, R.string.heatmap_threshold) -> { + advancedOCRSetting.copy( + heatmapThreshold = value + ) + } + + getString(context, R.string.box_threshold) -> { + advancedOCRSetting.copy( + boxThreshold = value + ) + } + + getString(context, R.string.min_box_area) -> { + advancedOCRSetting.copy( + minBoxArea = value + ) + } + + getString(context, R.string.min_box_size) -> { + advancedOCRSetting.copy( + minBoxSize = value + ) + } + + getString(context, R.string.unclip_ratio) -> { + advancedOCRSetting.copy( + unclipRatio = value + ) + } + + getString(context, R.string.min_ratio_for_rotation) -> { + advancedOCRSetting.copy( + minRatioForRotation = value + ) + } + + getString(context, R.string.character_confidence_threshold) -> { + advancedOCRSetting.copy( + maxWordCombinations = value + ) + } + + getString(context, R.string.max_word_combinations) -> { + advancedOCRSetting.copy( + maxWordCombinations = value + ) + } + + getString(context, R.string.topk_ignore_cutoff) -> { + advancedOCRSetting.copy( + topkIgnoreCutoff = value + ) + } + + getString(context, R.string.topk_ignore_cutoff) -> { + advancedOCRSetting.copy( + topkIgnoreCutoff = value + ) + } + + getString(context, R.string.total_probability_threshold) -> { + advancedOCRSetting.copy( + totalProbabilityThreshold = value + ) + } + + getString(context, R.string.width_distance_ratio) -> { + advancedOCRSetting.copy( + widthDistanceRatio = value + ) + } + + getString(context, R.string.height_distance_ratio) -> { + advancedOCRSetting.copy( + heightDistanceRatio = value + ) + } + + getString(context, R.string.center_distance_ratio) -> { + advancedOCRSetting.copy( + centerDistanceRatio = value + ) + } + + getString(context, R.string.paragraph_height_distance) -> { + advancedOCRSetting.copy( + paragraphHeightDistance = value + ) + } + + getString(context, R.string.paragraph_height_ratio_threshold) -> { + advancedOCRSetting.copy( + paragraphHeightRatioThreshold = value + ) + } + + getString(context, R.string.top_correlation_threshold) -> { + advancedOCRSetting.copy( + topCorrelationThreshold = value + ) + } + + getString(context, R.string.merge_points_cutoff) -> { + advancedOCRSetting.copy( + mergePointsCutoff = value + ) + } + + getString(context, R.string.split_margin_factor) -> { + advancedOCRSetting.copy( + splitMarginFactor = value + ) + } + + getString(context, R.string.aspect_ratio_lower_threshold) -> { + advancedOCRSetting.copy( + aspectRatioLowerThreshold = value + ) + } + + getString(context, R.string.aspect_ratio_upper_threshold) -> { + advancedOCRSetting.copy( + aspectRatioUpperThreshold = value + ) + } + + getString(context, R.string.topK_merged_predictions) -> { + advancedOCRSetting.copy( + topKMergedPredictions = value + ) + } + + else -> { + advancedOCRSetting + } + } + _uiState.value.textOCRSettings.advancedOCRSetting = updatedOCRSetting + } + + fun updateOCRSwitchOptions(name: String, enabled: Boolean) { + val advancedOCRSetting = _uiState.value.textOCRSettings.advancedOCRSetting + val updatedOCRSetting = + when (name) { + getString(context, R.string.enable_tiling) -> { + advancedOCRSetting.copy( + enableTiling = enabled + ) + } + + getString(context, R.string.enable_grouping) -> { + advancedOCRSetting.copy( + enableGrouping = enabled + ) + } + + else -> { + advancedOCRSetting + } + } + _uiState.value.textOCRSettings.advancedOCRSetting = updatedOCRSetting + } + fun updateSimilarityThreshold(threshold : Float) { + when (_uiState.value.usecaseSelected) { + UsecaseState.Retail.value -> { + val retailShelfSettings = _uiState.value.retailShelfSettings + val updatedRetailShelfSettings = + retailShelfSettings.copy( + similarityThreshold = threshold + ) + _uiState.value.retailShelfSettings = updatedRetailShelfSettings + } + + UsecaseState.Product.value -> { + val productRecognitionSettings = _uiState.value.productRecognitionSettings + val updatedProductRecognitionSettings = + productRecognitionSettings.copy( + similarityThreshold = threshold + ) + _uiState.value.productRecognitionSettings = updatedProductRecognitionSettings + } + } + + } + fun applySettings() { + deinitModel() + saveSettings() + initModel() + } + + fun saveSettings() { + when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + FileUtils.saveBarcodeSettings(uiState.value.barcodeSettings) + } + + UsecaseState.Retail.value -> { + FileUtils.saveRetailShelfSettings(uiState.value.retailShelfSettings) + } + + UsecaseState.Product.value -> { + FileUtils.saveProductRecognitionSettings(uiState.value.productRecognitionSettings) + } + + UsecaseState.OCRBarcodeFind.value -> { + FileUtils.saveOCRBarcodeFindSettings(uiState.value.ocrBarcodeFindSettings) + } + + UsecaseState.OCR.value -> { + FileUtils.saveOCRSettings(uiState.value.textOCRSettings) + } + + UsecaseState.Main.value -> { + + } + } + } + + fun restoreDefaultSettings() { + when (_uiState.value.usecaseSelected) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + _uiState.value.barcodeSettings = BarcodeSettings() + } + + UsecaseState.Retail.value -> { + _uiState.value.retailShelfSettings = RetailShelfSettings() + } + + UsecaseState.Product.value -> { + _uiState.value.productRecognitionSettings = ProductRecognitionSettings() + } + + UsecaseState.OCRBarcodeFind.value -> { + _uiState.value.ocrBarcodeFindSettings = OcrBarcodeFindSettings() + updateOCRModelEnabled(true) + updateBarcodeModelEnabled(true) + restoreFiltersToDefault() + } + + UsecaseState.OCR.value -> { + _uiState.value.textOCRSettings = TextOcrSettings() + } + + UsecaseState.Main.value -> { + + } + } + } + + private fun restoreFiltersToDefault(){ + updateOcrFilterData(ocrFilterData = OcrFilterData()) + updateBarcodeFilterData(barcodeFilterData = BarcodeFilterData()) + } + + fun getString(resId: Int): String { + return getString(context, resId) + } + + /** + * This function is used to load a new product database into the + * production recognition pipeline + */ + fun loadProductIndex(uri: Uri) { + val productDBFile = File(mCacheDir, databaseFile) + FileUtils.saveFile(uri, productDBFile.toUri()) + productEnrollmentRecognition?.applyProductDB() + } + + /** + * This function is used to delete the product data from the + * production recognition pipeline + */ + fun deleteProductIndex() { + productEnrollmentRecognition?.deleteProductDB() + } + + /** + * This function is used to add product data into the + * existing product database used by the production recognition pipeline + */ + fun enrollProductIndex() { + if (uiState.value.bboxes.size == 0) { + return + } + productEnrollmentRecognition?.enrollProductIndex(uiState.value.productResults) + } + + fun updateBarcodeModelDemoReady(isReady: Boolean) { + _uiState.update { currentState -> + currentState.copy( + isBarcodeModelDemoReady = isReady + ) + } + } + + fun updateRetailShelfModelDemoReady(isReady: Boolean) { + _uiState.update { currentState -> + currentState.copy( + isRetailShelfModelDemoReady = isReady + ) + } + } + + fun updateOcrModelDemoReady(isReady: Boolean) { + _uiState.update { currentState -> + currentState.copy( + isOcrModelDemoReady = isReady + ) + } + } + + fun updateOcrBarcodeCaptureSessionCount(count: Int) { + _uiState.update { currentState -> + currentState.copy( + ocrBarcodeCaptureSessionCount = count + ) + } + } + + fun updateOcrBarcodeCaptureSessionIndex(index: Int) { + _uiState.update { currentState -> + currentState.copy( + ocrBarcodeCaptureSessionIndex = index + ) + } + } + + fun updateProductEnrollmentState(state: Boolean) { + _uiState.update { currentState -> + currentState.copy( + isProductEnrollmentCompleted = state + ) + } + } + + fun updateBarcodeResultData(results: List) { + _uiState.update { it -> + it.copy( + barcodeResults = results + ) + } + + // Handle Picking Logic if we are in picking flow + if (uiState.value.selectedCustomer != null && results.isNotEmpty()) { + handlePickingScan(results) + } + } + + private fun handlePickingScan(results: List) { + if (results.isEmpty()) return + + val scannedBarcode = results.first().text + val customer = uiState.value.selectedCustomer ?: return + + val productMatch = customer.products.find { it.barcode == scannedBarcode } + + if (productMatch != null) { + _uiState.update { it.copy( + pickingFeedback = "Item Identified Barcode: $scannedBarcode", + selectedToteId = scannedBarcode // Highlight it on the map + ) } + } else { + _uiState.update { it.copy( + pickingFeedback = "Incorrect Item" + ) } + } + } + + fun updateSelectedCustomer(customer: com.zebra.aidatacapturedemo.data.CustomerInfo?) { + _uiState.update { it.copy(selectedCustomer = customer) } + } + + fun updatePickingFeedback(feedback: String?) { + _uiState.update { it.copy(pickingFeedback = feedback) } + } + + fun updateSelectedToteId(toteId: String?) { + _uiState.update { it.copy(selectedToteId = toteId) } + } + + fun setAllCustomers(customers: List) { + _uiState.update { it.copy(allCustomers = customers) } + } + + fun processHardwareScan(barcode: String) { + val customers = uiState.value.allCustomers + val matches = mutableListOf>() + var productInfo: ProductInfo? = null + + customers.forEach { customer -> + customer.products.find { it.barcode == barcode }?.let { product -> + matches.add(customer.id to product.quantity) + productInfo = product + } + } + + if (matches.isNotEmpty()) { + _uiState.update { it.copy( + lastScannedProduct = productInfo, + targetTotes = matches, + pickingFeedback = "Item Identified Barcode: $barcode" + ) } + } else { + _uiState.update { it.copy( + lastScannedProduct = null, + targetTotes = listOf(), + pickingFeedback = "Incorrect Item" + ) } + } + } + + fun updateRetailShelfDetectionResult(results: Array?) { + val bBoxesResult = results ?: arrayOf() + + _uiState.update { currentState -> + currentState.copy( + bboxes = bBoxesResult + ) + } + } + + fun updateModuleRecognitionResult(results: ModuleData) { + _uiState.update { currentState -> + currentState.copy( + moduleResults = results + ) + } + } + + fun updateProductRecognitionResult(results: MutableList?) { + val productRecognitionResult = results ?: mutableListOf() + _uiState.update { productResults -> + productResults.copy( + productResults = productRecognitionResult + ) + } + } + + fun updateCaptureBitmap(bitmap: Bitmap) { + _uiState.update { productResults -> + productResults.copy( + captureBitmap = bitmap + ) + } + } + + fun updateOcrResultData(results: List?) { + val ocrResults = results ?: listOf() + _uiState.update { textResults -> + textResults.copy( + ocrResults = ocrResults + ) + } + } + + +// fun updateExactMatchString(exactMatchString: String) { +// _uiState.update { selectedExactMatchString -> +// selectedExactMatchString.copy( +// selectedExactMatchString = exactMatchString +// ) +// } +// } + + fun loadInputStreamFromAsset(fileName: String): String { + try { + val inputStream = assetManager.open(fileName) + val reader = BufferedReader(InputStreamReader(inputStream)) + val stringBuilder = StringBuilder() + var line: String = reader.readLine() + while (line != null) { + stringBuilder.append(line) + line = reader.readLine() + } + val htmlString = stringBuilder.toString(); + return htmlString + + } catch (e: IOException) { + e.printStackTrace() + return "" + } + } + + fun handleBackButton(navController: NavController) { + val currentScreen = uiState.value.activeScreen + + if (currentScreen == Screen.DemoStart) { + deinitModel() + clearOcrBarcodeCaptureSession() + updateSelectedUsecase(UsecaseState.Main.value) + updateAppBarTitle(context.getString(R.string.app_name)) + } else if (currentScreen == Screen.DemoSetting) { + applySettings() + } else if (currentScreen == Screen.Preview) { + updateCameraReady(isReady = false) + updateCameraErrorMessage(errorMessage = null) + } else if (currentScreen == Screen.ProductsCapture) { + if (uiState.value.usecaseSelected == UsecaseState.Product.value) { + // clear all the previous results + updateProductRecognitionResult(results = null) + updateRetailShelfDetectionResult(results = null) + updateProductEnrollmentState(state = false) + startPreviewAnalysis() + startProcessing() + } + } else if (currentScreen == Screen.OCRFindFilterHome) { + updateSelectedFilterType(filterType = FilterType.NONE) + } else if (currentScreen == Screen.BarcodeFindFilterHome) { + updateSelectedFilterType(filterType = FilterType.NONE) + } + setZoom(1.0f) + navController.navigateUp() + } + + fun saveBarcodeLayout() { + if (uiState.value.barcodeResults.isNotEmpty()) { + FileUtils.saveBarcodeResultsToFile(uiState.value.barcodeResults) + toast("Barcode layout saved successfully") + } else { + toast("No barcode results to save") + } + } + + fun toast(toastString: String) { + Toast.makeText(context, toastString, Toast.LENGTH_LONG).show() + } + + fun updateActiveScreenData(activeScreen: Screen) { + _uiState.update { uiStateData -> + uiStateData.copy( + activeScreen = activeScreen + ) + } + } + + fun stopPreviewAnalysis() { + when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + + } + + UsecaseState.Retail.value -> { + + } + + UsecaseState.Product.value -> { + productEnrollmentRecognition!!.stopPreviewAnalysis() + } + + UsecaseState.OCRBarcodeFind.value -> { + + } + + UsecaseState.OCR.value -> { + + } + } + } + + fun startPreviewAnalysis() { + when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { + + } + + UsecaseState.Retail.value -> { + + } + + UsecaseState.Product.value -> { + productEnrollmentRecognition!!.startPreviewAnalysis() + } + + UsecaseState.OCRBarcodeFind.value -> { + + } + + UsecaseState.OCR.value -> { + + } + } + } + + fun executeHighRes(highResBitmap: Bitmap) { + when (uiState.value.usecaseSelected) { + UsecaseState.Barcode.value -> { + + } + + UsecaseState.BarcodeMap.value -> { + barcodeAnalyzer!!.executeHighRes(highResBitmap) + } + + UsecaseState.Retail.value -> { + + } + + UsecaseState.Product.value -> { + productEnrollmentRecognition!!.executeHighRes(highResBitmap) + } + + UsecaseState.OCRBarcodeFind.value -> { + if (uiState.value.isCaptureOrLiveEnabled == 0) { + if (uiState.value.isOCRModelEnabled) { + ocrAnalyzer!!.executeHighRes(highResBitmap) + } + if (uiState.value.isBarcodeModelEnabled) { + barcodeAnalyzer!!.executeHighRes(highResBitmap) + } + } + } + + UsecaseState.OCR.value -> { + + } + } + } + + fun updateToastMessage(message: String?) { + _uiState.update { uiStateData -> + uiStateData.copy( + toastMessage = message + ) + } + } + + fun updateCameraReady(isReady: Boolean) { + _uiState.update { currentState -> + currentState.copy( + isCameraReady = isReady + ) + } + } + + fun updateCameraErrorMessage(errorMessage: String?) { + _uiState.update { currentState -> + currentState.copy( + cameraError = errorMessage + ) + } + } + + fun updateSelectedFilterType(filterType: FilterType) { + _uiState.update { uiStateData -> + uiStateData.copy( + selectedFilterType = filterType + ) + } + } + fun clearOcrBarcodeCaptureSession(){ + updateOcrBarcodeCaptureSessionIndex(0) + updateOcrBarcodeCaptureSessionCount(0) + clearOcrBarcodeCaptureSessionPrefs(context) + } + + fun updateOcrFilterData(ocrFilterData: OcrFilterData) { + _uiState.update { currentState -> + currentState.copy( + ocrFilterData = ocrFilterData + ) + } + + // Automatically save to the local cache file + FileUtils.saveOcrFilterData(ocrFilterData = ocrFilterData) + } + + fun updateBarcodeFilterData(barcodeFilterData: BarcodeFilterData) { + _uiState.update { currentState -> + currentState.copy( + barcodeFilterData = barcodeFilterData + ) + } + + // Automatically save to the local cache file + FileUtils.saveBarcodeFilterData(barcodeFilterData = barcodeFilterData) + } +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/barcode_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/barcode_icon.xml new file mode 100644 index 0000000..250ccd2 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/barcode_icon.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/camera_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/camera_icon.xml new file mode 100644 index 0000000..7057457 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/camera_icon.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/down_arrow_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/down_arrow_icon.xml new file mode 100644 index 0000000..ed76da4 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/down_arrow_icon.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/edit_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/edit_icon.xml new file mode 100644 index 0000000..b00f82d --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/edit_icon.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/flashlight_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/flashlight_icon.xml new file mode 100644 index 0000000..ed1ff54 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/flashlight_icon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/hamburger_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/hamburger_icon.xml new file mode 100644 index 0000000..607bee1 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/hamburger_icon.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_barcode_filter_selected.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_barcode_filter_selected.xml new file mode 100644 index 0000000..9d98c5d --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_barcode_filter_selected.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_check.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000..4e02734 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_close_black.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_close_black.xml new file mode 100644 index 0000000..11e7131 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_close_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_filter_default.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_filter_default.xml new file mode 100644 index 0000000..b4f4033 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_filter_default.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_filter_selected.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_filter_selected.xml new file mode 100644 index 0000000..d91ff7a --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_filter_selected.xml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_launcher_background.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_launcher_foreground.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..011cf7c --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_location.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_location.xml new file mode 100644 index 0000000..b05b9dc --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_location.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_menu_barcode.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_menu_barcode.xml new file mode 100644 index 0000000..99930c1 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_menu_barcode.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_menu_ocr.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_menu_ocr.xml new file mode 100644 index 0000000..9d1d9d0 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_menu_ocr.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_next.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_next.xml new file mode 100644 index 0000000..d60c8e3 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_next.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_nextsession.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_nextsession.xml new file mode 100644 index 0000000..015c4e0 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_nextsession.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_ocr_filter_selected.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_ocr_filter_selected.xml new file mode 100644 index 0000000..564caf8 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_ocr_filter_selected.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_plus.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..ac12f18 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_previoussession.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_previoussession.xml new file mode 100644 index 0000000..572b5d9 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_previoussession.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_right_exapand.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_right_exapand.xml new file mode 100644 index 0000000..3665f8b --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_right_exapand.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_scan.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_scan.xml new file mode 100644 index 0000000..d7049d3 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_scan.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_trash_can.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_trash_can.xml new file mode 100644 index 0000000..8047a9d --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ic_trash_can.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/icon_add.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/icon_add.xml new file mode 100644 index 0000000..01dd990 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/icon_add.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/icon_arrow_forward.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/icon_arrow_forward.xml new file mode 100644 index 0000000..7806e3a --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/icon_arrow_forward.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/icon_close.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/icon_close.xml new file mode 100644 index 0000000..cb9642a --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/icon_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/mic_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/mic_icon.xml new file mode 100644 index 0000000..30f5ed0 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/mic_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ocr_finder_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ocr_finder_icon.xml new file mode 100644 index 0000000..96ab8b4 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ocr_finder_icon.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ocr_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ocr_icon.xml new file mode 100644 index 0000000..24dff58 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/ocr_icon.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/product_enrollment_recognition_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/product_enrollment_recognition_icon.xml new file mode 100644 index 0000000..00684cf --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/product_enrollment_recognition_icon.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/retail_shelf_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/retail_shelf_icon.xml new file mode 100644 index 0000000..4c93c37 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/retail_shelf_icon.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/satisfied_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/satisfied_icon.xml new file mode 100644 index 0000000..ed40c8f --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/satisfied_icon.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/settings_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/settings_icon.xml new file mode 100644 index 0000000..a56c631 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/settings_icon.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/shutter_button.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/shutter_button.xml new file mode 100644 index 0000000..5594cd9 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/shutter_button.xml @@ -0,0 +1,14 @@ + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/technology_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/technology_icon.xml new file mode 100644 index 0000000..090395e --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/technology_icon.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/usecase_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/usecase_icon.xml new file mode 100644 index 0000000..b32dad2 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/usecase_icon.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/video_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/video_icon.xml new file mode 100644 index 0000000..5aa945e --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/video_icon.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/warning_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/warning_icon.xml new file mode 100644 index 0000000..f673737 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/warning_icon.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/zebra_logo_icon.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/zebra_logo_icon.xml new file mode 100644 index 0000000..f8e0644 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/drawable/zebra_logo_icon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/font/ibm_plex_sans.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/font/ibm_plex_sans.xml new file mode 100644 index 0000000..b5e0fe3 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/font/ibm_plex_sans.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/font/ibm_plex_sans_bold.ttf b/AISuite_Demos/Project 2/Project 2/app/src/main/res/font/ibm_plex_sans_bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..258c10a96a9c949899883aa2f6b2794f8d1391ad GIT binary patch literal 218212 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%0Dt)?|ZenVk6tGn+p#T|0OZtI}57*Pwtj_P7} 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 z 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 z z%JT(P{U=5b5t^hvB&VSU*^mi<8K+hAxqK3#`%~E}MZJNg>ohb7#(dLcGpf^FdBOX; zn-*XlU-RyrrPbA?JHd8ushHO~XMmrw3Gu60FoU8J_jTM;O$Z8Egj8v#KkG#Njv_r5G7=DYGGCcrc$?O?1moib<; zRCBMVk)(5Et!V|!{Gkh*=hvX(7Y$RzhsF8m8>o6)n_2Y{crKxnz}EsDg~mrJJrH+g zjTUWc&@d<(c%HDl`9r%^S$lStRaKSk99IzGcp7B%_fdvh3lom9tye(-b|V6gRo`Q4 z7DCM`98=CD}oHi$JD#QiZ`EBBzJEsv}y*j?dh2%zCuX0AWe zanjJxrpz;rn|b1-Nhi*1>+Ec6>*~rpYx4m}u^Uf^W6d!;yI1t|F72B(t#4^B859|o zzw#_~G3G&Xix?DIJ_2X}huu)$xFL8R#DpX@cF8EQ6C~o$x--+44-Ku%Fd>=L(J=|o zFeHB+Su_zSl2%9w017)v1X#%}%(mykb0u47n9v5FPtK1LD8!J^zmqyK`Jpzk;79Ur zE|v@_n^MK3Kt?dPYi9L=ro%(0wB$aN0MbjMqrB~TYbr*sC=bqH$w82295eLvO1MrhAr!A=9_TA zOJ>LPdQ)W_5D$&q@u>2Fe)NGN+7+aTR;b_7ye8hgx_Mn*Ww0_-kw+2=jE|!;Ip`m_ zrkR`+YmiZ@QJ{N3Ts#7!G(5ZkYgXbUc>qs4!JEm>C3EetykHALvb?B?0J3QCCq;8H zEHCiNX9Xr}c44_kJbvQF5wT8Gzv&8kuK2uo!{sMFclgH7uejoK@xiWjm#twAos04w zMR^(EA6OCK)U&La(ryLo#_S4oCAEb#BtXnTr<4lK#w?N)oZP;rE_QqA8fM4P^{B0f?i@{dCI0}T`--krt|+#J%U;wusiZaUgrVWh zC+4-3OzK*c9r5_0uEDOp;>P?_HV+S-nqMDX|6r)dX0-)^x2%s=zCfFCy-*ole@ifc z#G=rH>x*k%q3v{Dslg5nf)=xPDht~>ooCxM*vle2Hee<#o(>Q(2MFI9XQKeewW)FVXIGs7t$C!o#Z&; zsfvXwd&)$n6}J1RNmfy~@T}^2;l=GW@#4;Dr=C>N;_v8eT~IKuVoU$FUGqB1dn>!P zWlnADE3fg^7iZKrulLpzr&rgPMY@~Xk00opmu|JhrZ;!3ZUL0k&uB$ticJM1i#}oo z9RW2^7lRXJQ=0%|bHk=01~zBty(05?{LC2n5Gx>0+jE|{2D5~h?m2IS=OH;?{YS8) z+z9Sr)A#WVJEdYeQ47#&EYo7LyGY>g;Ue)ef2SBwMxkJlf+-Y`7`*OF32|Wfj9uQw zl9sOHT}7UPm}_&#jBvg0!ZQHt^Dmai*56s=gPFW2{LuPP)$3?5g$at~kV~Tv+fg)O z7*fWU&oKH__Po4cohVvl(oe{nvg{kHo;lnr z2exha_J;2K0o*y&6YT$wVx5ru|RCiLez^j(r1JX4- zf{lr(craaqKP6{kjhqoF1X?DTB(7mQ8=X%@Nw8W#>%0#Zpu%`IbSNH8urvv~$Wh;g zMbzX$=&mr?ptHgu`NGZqLp#oGv(ya@|1&$__|oR8-i&y=4B`I zyevgh#ooCsMdgJ>8HGNPotdF!i24k%CPQc+@sF3l4j1Hs@S}> zsgd_yhZl5IoEcqt(X5&OwlcbJminxZs$@ z7tUM0eBOnNkGWvp@_WP{e@$l$pBjJi1ndnqmA(2@cs`nTg}gvrozq`h>P6hGOzHJ% zG`odG<}3^uvf0;y-gKfFoh5dQvqCE`o;~~G!QhtQ;5X;Y`Q~8oTaoaT#nsivyg}cK zr$i<#oqySy<1U-Gbm_dyj$3ot{H1q^hiaE}bWRapamRcPSl{yGe@N1?wHIQsxa!x$EJd3;#3m<@NQ; z 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

    BMSC{T$4l_*e&0+lFGi2{`< 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≠@`*rf4E6Otw;4dl$ZZ^O9zT(^;w zbGGdXo}%tHmghR#HlC+G*KGptceZUZZ-3@CkQzDLcAH;svcH2XC;K%jPc1bnbtyIE zz4=)ChVsPTli2b==kmtTMr9H!!*tn<%Wd zV>L$AZgrE|18Vz%+EUdIEAt06K$WW>Y1#aw@iS-#9FGFW$>1_YH&kPF3()*~i{|kb z&CiHVQO{ySbyCmktMt{hoT_gAy^UHWeIw!TN- zqvq;C`hN8e{P3Wfr$@jQ3*d_1sgLwy`dRgDg+No}=fg zkbYNxq;}{};FbcoIeNToKs~v=cwMGH>hJ+ctLesZ^p{2)?0K) zoz&TEN=I~5YrRt!pe2iRiEga->I1r|uFwZ{D_zNUA=>RH-9{hRHM*VA#?_a?hxPP- znEK`d-O;o%7wI0tjrvCPT~~dR=sSHgJo#&Vi|9Ljn`k`U7oB&f{tbF>fbJ){Pv3#| zyI1!Yjzw+`GsE;<@awhoS|i>fz|XbUi}!pdN`PoT*0(59^1~ zhVSVy!pZs(bm0;`7G1bZqan;n{RF(7smBR#>+vSn?AFhiy=I?&-Vk{~Pcqe}T2B#f z*Dr~#)YIVk2KrCJ^ZI4sdHt%>!Re%Dp*_3nzo0>T>Dfqu-uiX1?ev@aEwO*F3%g@| zO~Eo-Uz(~bf-J%P6+!+Y@?M}QNx|>*kv=ZyzUjN2y1H*)@Dn$ z$Q_I=n2lYfoY~l6k775?BL8#Q(y+q*^)?Ypt-H55Z?nzEZu{jegXiLX&fMe;WwXC8 z!q%K~#wO2l=Gi&48NQZUkqw)cx;eKETh(sVVeMvfp+?VT3t)#1_cm+w=2|OTEbX7$ zikt$wmD+zO%D;5_w#$2SYa`45c8;4PR<``ZV%Hy&|M?uZM9?4`?S)g@EU+=l*0Nw~ z9mj&E^J7RN_e~&3{)Ls+j$g#DV#{x`7W)*g7mIy5zstWY7XDl?>gCm+yg;LG;|u=e z{}d}MnkeJ?G3dySrulo48_qa;;~;Nn<3Nm{4c^d?DFjXiE-(}gy@(57u|>b+e{ rd;FH9>aa~2H%2~}+w?q}-fq(hn|8Blcbi@*sbcIF=>dCY1gZL85c+v@ literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/font/ibm_plex_sans_regular.ttf b/AISuite_Demos/Project 2/Project 2/app/src/main/res/font/ibm_plex_sans_regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5387ad48ccb9dd751aef3f1ee7122b5c6dd56946 GIT binary patch literal 218236 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
    Wb2hTQ5EP@lT!aID6U0GG2(= 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 literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..bbd3e02 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..bbd3e02 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..810633e870de3eac02093ff18e4c1a5cc943137c GIT binary patch literal 2482 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 literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp new file mode 100644 index 0000000000000000000000000000000000000000..2c0847e8e41e69256aede9598048bf9a2bcbec8d GIT binary patch literal 44 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 literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..cba5bce725444eb1b10be72a17bc09f4d4997d3c GIT binary patch literal 1734 zcmV;%208gsNk&G#1^@t8MM6+kP&iDo1^@srFTe{B>LAFr+5EA;J4D0;(5iJA8czQI z>y%%gQ*0yvqt38W!1)V0y1Tp1zU%6&)X<;sxBV02q$)Kip+U$%7iwM*O5HxGrnmIM7^RNLVBJs?CJGayP zwzB|3(9Qt=1PfU7cpnztx%uZQHhO z8(aS!xQ(Pp&N3f90#5OS_y^bYmyluOd`^;=fP@L2z$GAgPXKmIui}yV(>c^W+Ux#9 zd!Z_}$9t9bx#K;t{m7H^j_02v^QH3(NKQWXToOm--|Rs5xUPf-tZw0DDgrZR-P*%e zqy6i??m4?glo`Xm<+C_&elScGpnU`=2rz1Fc#_Aq<-8_9x};CTrv6ge(>?DaQ~~Ob zy%dC>^25)d4+_$~G{n(8$~=ZelRBuwLRjQ)#>;`eho13{Dpe0v%1*GJ}>PnbQn`_M#tsjYUV2({iPC2I5p zGc@lQvmSR3Jb1iI4|Qe%MoXr}m<>boq9w?vXtUxyauA^-sNXD$(37knHVoS%fvQ15 zlpt>6q8OxFo`{GHGjMw9vfe)Pv_#5aq3)|WEl4$Uf?)~qqv)tZ%-K+QS}^*oro2~9 zW&%(6S-7L5sL&E9Sao=aI0_30Twvo@vtgmnA}ph4E?y&p2^dh8ezIg(!_d{w$AS!m z5E99VJr#dX@QXN8!Xn*JSDibV9&Tp|h?fOUa4}oo+|4M)E-lnwRuIaSg|PrnN3g)v zP|Ba^pUmhbt zOYKtJ3oehIU?e4~A*+WOcp!(La)54bgpv0DCc3NFj=`%l;hs||zobPYT#w>#X-HuiCL5BIcm|1aij64lgv5{g<_ zpcskwN|43g2CH@w;O9QXB=+u3#=B6_cPLKn;hZN7gbY-TdgcoHz3Q~qsrd%1TFa5~ z_n2^-DQ8A-GpAE`(C@9Tx$rRG_|MrE^RW=Yeh9G0m@qX+@)nssGk*~1@8Xr0NHZ`G z`xvwKz9A%3tthjwk<=maT~`AB-8m$`=a{nvm7e}KGKB!kaiO{|VAe4KJC}^$cg@ct zwb~hrip+9bF_FX5(|=a&{>2Pz_#Pi>is*mCXBLdUZFwcuZg0Kh!h@Zd_wY-fV(z9> z^P@~2?JEl*M8xvywabnsIox=Zx%bRz>)NC2O&q;M`WAh2vqbv8M-pW5A!Ti;@=uck z3|5d%XQ$eh9rBZu1^!npf4m9NUEzDI%I@A(dbzORAC?dT=`vM~vM;#s-~Po0G9L>N z*R1;mAtWrjrF2?NdlBH>{TxKBNALgouWzDupu}^0QE>{rvLx| literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp new file mode 100644 index 0000000000000000000000000000000000000000..92e3a1a1bb3f27d2d05abbda70905728f13e5bbf GIT binary patch literal 44 zcmWIYbaPW-U|K^ zw*)o>1SJY6000=0=55>lX4|%HCfl~rY}>YN+qSji$ZaD>Vrg>P^FATJVjHOR?kZ>w z0DLDSzav;}fjgf%1oe*GUU8ZaI_+%jIb-_OD-7@H4cQ$6hI3N22Ynve-P-8oF&r{_ zt9JKcT%A8NeULq=`q8iEG=c4GcW~a*4s=1MZL5FmM$@%=9zpJ3oftF(^+mwlr3Byy z(PTop*Y3A9hLmPpc)#><*g$Dpg8*i4F>|(JTaaK zbXV?z?-Iyc{b4aOSGA5vvZ{Bm&f3C$YJ?|9w`v0mX4oMBKnXXd+SNhZ>I*P3SMMTd z3rs+gK&V&2wpD+|Mi?Ljw{Y%%2rfd}>Ioje&|K3JnhX4A?Cko zcY&H?3>d0w2~KjL2s%)zZ(%)DZ~_k0{YdfzFJH$8E^V*15L;_$YA-Q;`I#YK9TITd~Ku z_-aIN0LKa<24^)}Cr^a15OoHuwmQ4D)iK0Nzs}d=9U=k+QPQ>cCmyQ%fxt!sC?a(e zbR;e%TqF9?lMuuKTBrtfR|E$BB~a~Qj~#f`^uiSV5`tr$s}a57QzybEsC8GUtB##V cqk^p(&K&3n{x^TLuW#q~7O?fI?flAV0=%Rnr~m)} literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..a25b5358dd4549f374f46ac218f51677a732c8f1 GIT binary patch literal 2670 zcmV-!3X%0vNk&Fy3IG6CMM6+kP&iCl3IG5vFTe{B>QIoj0kitIy(b|eCO{tGW9d}? z|ICVhH3~RXloDI)abwKD=&>mcD zOYGLkD`?NVJz6%GI|lC7+rrVd?XdH1B3gruE^SI~!7Po6rES}`Z9C(c^Ydf_10j-Z z*UJ1zy!V_lOICx4X8|2Zhilumv1k0YZM%w{bhd3F)M45|P6Knizo*Xt$D|0T|$>JHbEFZ({=*nMSk`^l3;VJ#%1F(3}K2_z0hK zh<6F1F5uE2-&JtxZEQfh4cF?BYycudumB@)&VPj4$nKD6ZvPBPVkAk5BDN2^{`2K*H&IE{Q zX$NXX`X45X2(#&I3JE&F1v$Y5jj$6u@8qfEO)y!0Dt`Rt2*TQ#gvZAJziCZjfhiqR zB6}v!xyf>4iadS$&%U>Ff4G_0$RHOCaK!W`NAawiB5$S$P&^=-U;n$?ZGV!0UGX6< zh>k8Oyi@Y6++=YbB^3{>SfD66BzInW2m4(dtOJxcK8nYDl!^yZEPsWJrhXcV#r!9* zE}l*a>y^kq3k{Zk#^L2cu2IbTIW2Z)@{AniQipNrEF}%%Sge)-BmkPRb%h|502sr> z%BrYxs8+m2B7a%PUly6}P_0$zb_j&Rx)A#f83Bl>?H!0EsqdNZ%w=6P zNC6TEMb<@~@5~_xS*OcZ6%w3{A7APy2VG-H@ z!vyiNuV#W(qnNWK_N<6CD-?oPqsLu)m|HRAJ9vUSM z5qp^L$VT@`d+#0tq!L0QPr&GVi(q5UhZS;Mr(*t*=n=mADbbcikwunenH7li6D^M( z#*4?}qDK#>pXk6Su`A@+W3a0}lmTfl|LFSat;0xV;rV{oD3&*ddwWvf6H_cmFH%an z7c8(0=teR}ns7B>mYE1UYGkxNq^1vPMCd^sK^k1hlmP+5w}ES#kWIs@S#;DnM@j-r z;H=~`GH3;qe}S_~4>(W@-XakX<;$=#G7r2*iXc_33~6wI5`pgyyP8U*v+!$9^5h(h z6Sp|C&Z;OYocyj;1(NQ$H1~|6pb2MQtsYQJzSk5x#-3Hih|3%l3#3@Un1`Nwlf|2$ z>7j--QK#NeIyZnO48_EIBGDdVvMcBTB-u0e)I=HeMPcZAn-c+OVA#YvIbi2p!-aY1 zj$l>Cqemdf@V*7jI)$>6>HTTpBhpcE=Rty114QXb&df!aqplw>YZMg&7_uEvh~!$s zcV_b%383J_9B|0^ezmRqB20D56?Zo|Qd>F@Pe0P|;9(N(J`$(qD8|f#XOERb!>i<` zO>4`6LEEJY(L8EYaBBf@&w*Ova7!t-d6wYXeWib&!gU{~=7jCxEs{j-DI{uz;M#os z*YN2+b?sexpwHsgMBv8!P`KQ~lE{M%(W1^U(mo;$!;jqKs8kA?wRGAyy_$et6C*V| zxzQ&)M;xdSmGtk+(W=IX3vEwLrM^c@+}ZS6#g^(a$Kqnn0!qbeB=QwmG<<)#ZCYO{ zphi|PB3234{SdsGxbt9?X-%w(3fi{l$@u;-vl)K)9zZ11R-9F z@kqnEs7rMZ4h?HfT}U9U9q__d4<@^_2~zGoJ8FnNHC)@#=%+CYAYyveKl-u-vy>sFc>;lOd(FZY%?2_k7mdWLcy{h`nAX9o z89w`sdT=lUfdTw7H0^&O`=DE+yh9TF2#uk9@wN%Xp6^@iK&9T{$AKJKo!aXo2V3spL3~2ewGL-Mwu&Z(k5ToM+C_Pq_jjEsH z1IGLuoy9>15<14OxSJigcCUg;%vsbWWVLf(mI|{bSwKa;8}s*#gV;alTi)OKsaN`x zk{ome2GE-Tdbi2d_wyj)!}?`Jq#lvxUdUMyWt1-&y-RNEQY)h93jhcQ1uQ|Qfi?7# z9VO=_4U_<3|LwiJSdyS6e+xYDRSnmn0$z6Xf7@mjCt zy;k(=yJ&SJCqSX)7htc|+yufhH;&7y2CI5MRArCvE(4Ig*ED3s7B#a#<0t_=Z_XI)rv7ml5|ckEk2qbdfxWxd5>8|w+_Wq$2(4d6~GW+XxH7{ zqaORLfR<(QKqn(SXjv@S(XOj|1Q^dhc!#)rnH0b;(w<`w!=RmwbtMefLrTK>$yHu43{-RJ+#KwmxfnZPQKWf&h8xp@OBHlL1r$IEAvI8h=M+Iic!x$#MhPBM1KAszxn-U7 zM&wRT26ZqurwR{Wiq>*^qyh^m;FUlzG(ZR5!3`P8I?u1Sb+uZ-1Rm2E&)GF@E^vc=+Ob z4CWGj534?ybz8XdS2J=XTWg++QvwiYxmOVRXyEHri{s8 z0sh@8I(N6e?mg}21j4YwA__t@I;uYxaaALrVj+OjAUuxc|5AV2*ul#L+?(<%zpI?r z@en}d+KR?Gk$kcw+b{n!(}=s^j)wqlnzks(l%!4niGf7`l2B>`}V^li~j}!~aWl zugUK&$zJZeMCyBWkm>&S2SZhN9cEA(@B*1}G^3QujLk&os_Fk9Q{79XnG94lwWwwl zB3udcR>YhYFq?JFTzk}_T7j}Qa!=Wr{{P0F&88x#9HtjUb2+5}*Qk5N-Cd^rUy*03 zQTM1)L`wk*?&gbEH|AcEXIf(1-EP?3N(I*ViX%WZA|0*=ENSi|Y!B3`IwcYW07@jN zwG!c}RQRe8k*jC9z*-mp3fFG>;d>~}|5U?NZ&e~;Y$Xw_A$Lh}k814MxJ(0zCtw*M z)uifeWNwIRznpGz`CGErq-p^sK&!_rS5=eWef?%z*r*Bzu6oJV(n}5iN+3|V%+O}$ zC)^X#W>0xfDefWO?#J7`6nAg7+sHZn%L27{0s&=$+}g{MZ0%;b(};>EKU*ot zY&4*3&<3trm5eoO%|+z91K=kAe0QE$vrbC6%LD~5HRz6M|ED8Vb!kT93|?(axW`qA z7~ssA`rc`4C(9~i4n1G5Rmkk^XO&9N0#2ZMRYC}CWv2ZAG!tPemoJ}L@a`tOU8-6n3rh97Jx(!`k$M7# zrz#PT)Z>ggbLBA;kdIVXFrZx$yt7uA>Qh^J=`M`2FB!C&ARsU z9?dKMVGlL-Yz+vf*Cdg9I#@NA4LU&S*Pti>%4g=6SprvW2?Q}!r*b>_Ky*HFcW241 zfrj1nsEU~j0Gv%nNV2;zC^?(}R!DMJk_Q0I&sS`XyD#%;_@}2_x0wTQHW`65Fil2) z6F?`j*$bFtmf@PY%HRQD;{EzPLhE2OD>9nEcspmU#o#Gfoze zGb&(KKpBEhb>V!zMN}mdGX5T3UbzUNOpxxuikSyM1Yhg%K#C3 znB(ukI$}E0c2vngq6SrFADUs7_^Upy53(AO!51U<r$*to*lJb{_tm0<5`&xb86uuI5j4-2v5#?42A1v?@hRCK<6u z7b_WX^gHg!q+Zs2#pFtQH*zdbSTW6gimJD44@!2o_BXrtpj{K~S<<^|!0o+pbW;(ilKdcfdGG&h zGYdL%=mA*;i-lb&|N?BNY%qQ&VZYO@h8lRENCYb(glcCu8= zcBzT*V{X8o-_+MGnV+cu{tmiHXLLu4FM%z;6yVEzmzeEReko#)&5xZJb5`}XtM(JU z1MX))yKTA$uSTgr|2WOPFDK^C*dP4iiU_y|kJQ_)dN$&2F=m+tSczkn4m^5(e{Y3F z?bpI|A9KRw&l^o*^gBntlbdL`9fM>&M0KWmUngFyPL@{;r#L))Q8FPuA=PR+;__Nm z`aYCERn0(E>lCgOAj9<_TYZ@(U;7i*|--qJ$PXX~#u&BE0 zn{o8(v-WBt^YzmJ6p}x7yIR_FDCsdxve zKUcW}MyL)E6B9fKQJ5Nj$!L*+OUbS_A+8apyIqLnitoARe<(fa|vmob`*ZIlq^? z^ea%-JpYSA>vE#k75~3^u;T8OY1(uD=#Kv#{JLm|kTh+1lKH(fw-!pWwO3C!=qiz@ z8?Fzf;u%&C1d2o>iUcj=!zzNv%{pQY4JwBT)T?cpw zkj2(|bKUA^7yXCUDE;Ih1ztc|IG!-nv$&Ria*`LD7dKZC)z%UMe)ByFh?l>0Hh#@~J literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..aee60ee2baf57d2885681a5fc9402bfede431dc3 GIT binary patch literal 1204 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 OD|N}|vn-)s3;+N$k2%l) literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..85b0afe7912229ae0a06231a003850e824f265de GIT binary patch literal 5322 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 literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp new file mode 100644 index 0000000000000000000000000000000000000000..a49f6d4d4dd47c76aab4c5d96db08745cd842a28 GIT binary patch literal 52 zcmWIYbaT^TU|c|{`G(N|L^|) H)0i0muEG?J literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..1d8cb8ee6a29e11ea4864e453a03231e9e1c3a4b GIT binary patch literal 1738 zcmV;*1~vIoNk&G(1^@t8MM6+kP&iDr1^@srL%~oG3JSLE#%-j#|6P{_K$LnAdKBS= zfS^Pn0RX_Vk#Zg0KHIi!+qP}nwr$(C^|oz$jwD8s6iKr5gXLW2o__BZ&g;L|f3N>u z|GoZu{rCFszs%4P)Hf{>%BBAKjDC;|rh#U(19hVy3ToNtHkY~tHVujFZcxicvrx6j zmmvKXoN*VZWrJOZK%<$N2`$sz4*jDg!8Gm!wQ5jo5a@MPFpWDvts<>MS8`d`ia-{5 zaCfUUng+CJxT5Qm#_b^O0<~tKU&w7BE8N|hk4H5lsJqZDwScJ3z<*|RCkM4|Od7X> zoP)nHV9_tZ)!YJdMr(k^aKGS$pv|8r>84;BH-pqfL8UPe6ghuEXswdVc4L6JK`{5t z>DHui6G%aGpa8AOjTcK?^ZC=8zJh6Vt4OyejT_NB=m@>NcjwZz89j0lR>#2Co!l&# zMmKZN{_1@670EsYbxwEM6c|i%ZV^nQ8%6rQkiH<%5=e`HWzMY&>4jjEB1BLGEEdvr zsI#RIZ-~!>6u!u9f@xfT#prWG(u&|G%yVwfUv8`EJ%M2~O3=i?wjcCD@T?`e7nksW ztDypPWqu}vSx&?&rZ3IJ@&mnQT-B9CD!RqEvMJ$GgvkvW6RtVoer_?YXipg5V%)1g zX>=c!R)kyCEx7D%!8Cf(iwF|TVRQ%1gLY3|Q;&NQNmX|MfOc;Y%uPv@JCx~BFCuyJ z+~Je^ieTzR_u8N|s=YMs1l<9I?=+;`YZK{WKqp48T(r}WdNaYFRu~uN6BpDaCinL`j?I z_iFgBarJ)mfqNw)icncR5|408&;oQJl2<}zZBsX_(6z_^qB=Z!BbbJyorcKVUi?R& z&xwQrU^gV9M7d_iC4XL$?K+et%0)g>EQ-K*$R8-DKy zz9QoDD#To~!Q!POkpn&aavrez5phMqm4OyLJn+F74nQjgKD}Fr*uxFem543smks|) z#QoB^h$-II0{S%w=zTu8SkZ1>M0YL(i->An`q0dX>|6-xV~&UxHiY*u!a5hiMObt$ zL~=v|T9-a`&_-0BlI|6CFMaNyjhH?k8rVkz)0Ym~U)(xq@7I6-pM;Apc1by~ab~K) z9k5-HcOVf{E{0$BDE7dND-~GdE+{@Xrwf*&n@~YrZlRIA^}pIee@Oxb*yvSz2=5-4 zGvX(VdmbC~`d-6qUoqpn6Ek`bf1<0Aesb!mxkRv}oqdsraGx1n1r?8klJ~&;p@DL_ z8^h>vfNmmUTn7IH3$$_4Y}kv4NWa|pqvd$)1Jv`?`N~Hh==A%b)t%_?j+1bCZxC%9 zTo>sXU~iPdMlK9=Jd>|{bS|0$3Ri5Go!V|XXfzz?Z9YU0KD-@&axm%7K@0m5O&*9Y z5gt}8Hqg8#9{*%i+z@_&2$6ztIEFzbK>`jWT@#KT2y4mx2R9tFdB6|fa3Swt9{j-4 z!sU|_x_D1jL=1<3f>ERcPDp*R3(KJW(7qz&_ez)Uj8j?&myaLm`%gFm9>V)Tu;9tK z;h||NysL-ts(98edYo=0UicdRfi}+Q6X<6?GYC-*F5j1-5w1))tyf_SXyVmpgZd-W zT6hnS3jAR>AL(D*0;UQm4<7V9-y_`O`OxGGniW}p!iej_}Ctmu!jynaas5Sh7nrv zv;F6VnP5r3pt;I9yZRhcqhS+#@d!sV2b)e@Mc)rDeO|g+b1vW#ebG3EfD&B#AC}SH g!2^Ia?hIG%7S8Lx*MG17UjM!Rd;RzN@4px}0On9sy#N3J literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..9e4e2bbe81b706a37fb4c586df2654e59c4f23b0 GIT binary patch literal 8862 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 literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..81d51a3eeb25cbf1e3c74faefa7139994d4e267a GIT binary patch literal 6914 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

    !x>*w$N(L~X5Mos3u zldHBDOHBzPSL?h>*6|a=zj1D>ciktQ9U_aCd@%%nWYxVbq*rBO5M~k35de7rRIOaW zKbBh4)bx5(kDn~m`X9-Eak{&f2#+e;5MpZb{0#uLp+YdFnE=EPXwMW+rV9WlAH}j2 zU!%@X4Z_9vUqY^jFXm3yKXJMxC-4Zp>V&|&#cvF%nOc2y(Il$y84U#hK_2L`^FvV% zK>hT->%+_0Ul{L8$X~&(>-dG?fB5DeKUuoC@xO%r{N`)Mbncxw>Db#048&sq$S9bo zECHYffVL@g`xln9JyM0>kw}kPz570y`ac`|pL)fS)d+$4E!T{xUb}erJ_QmKrJfj$ zBA|wf#OrDRXat~RLhtd(MasV^)exXodzPBRF+1Jqptb!__L3)L{}S6bWv4qCw8uF+ zManb;nXCAPQJvf80O))>Ed)Rm;Dk_U1^}4=6cvoqGyu?&DMr#cpl*82)YR3xg-bqJ zxynW5#t4fpA28BuIo2>?UzvYoq_=Y7t29Pgwc5jiCGSi^oWAc{J-RM=!;9bC(a;12 z;sF(aTmZ^)#Y&rUK-!+1DMr%Y53hgYShD4dQz*AT&8Ru}H#@m6M_#a)J}wNp_bZ*2 z^*%2QdgKMGnT_06v*?cgojQhW@lxO1zRj6pBm>FEeEl*2xoUtDN}&PZ1CW02)mb2G z$Qz^Td?iziq&riLq$gu^;fOXe>lby;u*JJ)+Lm-7YhTi48MUO9F7=XDx|B;~`=>U` zikH0A9n-e7TZS!u^k(&=Ua52T)%Pa3HGzYKX?X6v8i2aYkhbQHQFRqc)OTl!k#t4X zXTYfQx5^u%?0zHh>U%wgDF_QU4NnMwl9ZDGXvz#}`-OR8lsyGwbiD;*bbWrmzhI)W z?}R*QlD<6IlD_N(=#$x-{o{la{C>axv&ZWk{Nza~hlD(a$Hq)$ literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..bf19fa5b8c8bf59b4a4223ce3f674d7c714d334e GIT binary patch literal 2214 zcmV;X2wC@1Nk&GV2mkcmar`7`2?a+Wf)N04fKU(y9E6H6ghv5@4BNJCE84Ja+qP}nwr$(CZQHhO zqoMa!@fCe$MrLK3h@1-*(SKQtBw1<{RVQY(eyjx8UKpFXU$Cxt{`36j`Oov8=RePX zp8q`mdH(bK=lRd`pXa~-ENhRUo2%<3uquD#)xd<71@S>~+k#Q;EJH%~(CKUw5>NZu z)dgZ$-5xZ5i5Twc9==U(3DWJvKN9ZZ9xkv0)n1~Q=+5r_%3uRVI*3GF!x1)MY&25s zqkBhodit(z1!@Yq@ z2fA(bMW^7qG;a}IG^(w(tH5{Lgr{bDxu~v*eAh(0-orhCN+-H)^aQf9U4p0RzOA(^ z2ez9;JzL$wMK!!n&-cp@NF>}HsB{jKZKV`$!1*V#qqN(FZcCMs0Gw}#Y&W{mbV=a> z!27hI+ZCvEC5NGwgy)rk`)L`a-EQ>G=AuvIk^}$K!ccc1{ysM(Pevh0nBP`(=_w=K z!>F65Ihfzp^b^{2dpP>dW)nk2T@14`Tm>IJ$47fo)+27E}wqkn-ULl~XA= z?WpuCNo3ncyKxDm%xnZq&jS=QUomCGgav_yO!pJDy2A(zHsCoVGS5fYY6b=xFdxqK z85Yy}92cT%7}wj3?i`;{LhCVq$_UQ(nTCFjw^rk7ra6$rz$YGDK0mPu5pm}HQmHa{-sVNGs8HZcZ`EnrW*(O=$>S9$I`=Z#zFo!NaX~_ zYaZm|c*i-BqcJ?5h{qitSy14?ek%PLtFMn7Mi#uP!Q|()7m=M1S)YDa=kYuQ#iXo> zk%hmiF?rs|LTVU$V`TBT4-jzy*-a4@)2d9Kmo*=#ZjK}#_W^1&f_U5q@}w-PTOtd8 zR$=ly)w$$wYs5&6$6b1jER4@&IEUI9XR8&#*+6xdy|{;(O?uW^l>SOj%A$5AJNqY4 ze20{!30T=O-xPCvsGf_wM%3RIPR->0iQo-hwk+KlbpJ%~Ixky}?sT%}BaUt=UJk!& zH|3uQUgKrU(VYfVB8{^xPj~8{hqAf&+9`ih_&g^s`-85&tA;VR73kq)ZO)L$!e_i} zMS3_%iRfTaE;7# zX@Kkyu%XoP({p^RsCDTNW=W-Y4zjNfw4&DYTUYDT!vXfy{#MimcGqw}J8F;|_O+rm zq<40(qBbIjtH6#LvMta!Xj`C3kYG&>$t7ytk%w~C^Y${ys(mAlhb<;ru0BgLPLtqW^*%9GX+f1O(a;z|qE z&L+D9F^P9fTDi95GLmV}PD!N817HN4&;vz9FGV?|8=h*DpP#OjA(zDehf^w**y3$1 zAie0YXpz|8$(bp`7tvlYqf|(61ythw5WEu|7k}!>byhn0XQuh$l^LB+;mT+!H3+i` z7p^erD+)R)Rg;cZkUk{-91G(_n|Uey5Bkb9cHXx=mP+nNqP2pa@~XG;h6P^|aP%^& zfJxCS5(42>=gvd7{gzK0$!#nOAg$}7At&VwiPnHY;YG%YxvDs5$_|kIPv%I1>!f2(9W6Z*_cG@2Gy5If`0oz|6}jU{XWr+bMP4w_W;zTt$#J zsAmHc9TKm^cPWiprbe8K+!mH>(H-@?j>E%c7)e*+mAP_C5^l?f(nujif9Xcin|xel zE{YjMZXa4JC4fRIa**$o#*ZVZ+zyE9k0NJ-ndjJ3c literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/AISuite_Demos/Project 2/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..fde2bde4fbd428c1b17720a6791373ab660b9c24 GIT binary patch literal 12448 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 literal 0 HcmV?d00001 diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/values/colors.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/values/strings.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..f5b4c2d --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/values/strings.xml @@ -0,0 +1,224 @@ + + AI Data Capture Demo + About + + Use Case Demos + OCR & Barcode Find + Search for specific text or barcode + Product & Shelf Enrollment + Enroll and recognize products on shelves and peg boards + + Technology Demos + Barcode Recognizer + Detect and decode barcodes + Barcode Map + Detect and map barcodes with relative positions and labels + Text/OCR Recognizer + Detect and decode text with advanced settings + Product & Shelf Recognizer + Detect shelves, shelf labels, peg labels and products + + Save Product Crops + Capture to Enroll Products + Restore to Default Settings + Advanced Settings + Back To All Results + Results + Results Session + Settings + Start Scan + Scan + Enrollment Done + Load Product Database + Save Product Database + Delete Product Database + Apply + Cancel + Back + Home + Enroll Product + Scan or Type Product SKU + + Barcode Recognizer Settings + OCR & Barcode Settings + Text/OCR Recognizer Settings + Product & Shelf Recognizer Settings + Product & Shelf Enrollment Settings + Recommendation & Tips + + Australian Post + Aztec + Canadian Post + Chinese 2 Of 5 + Codabar + Code 11 + Code 39 + Code 93 + Code 128 + Composite AB + Composite C + D2Of5 + Datamatrix + Dotcode + Dutch Postal + EAN 28 + EAN 13 + Finnish Postal + Grid Matrix + GS1 Databar + GS1 Databar Expanded + GS1 Databar Lim + GS1 Datamatrix + GS1 QRCode + Hanxin + I2Of5 + Japanese Postal + Korean 3Of5 + Mailmark + Matrix 2Of5 + Maxicode + Micro PDF + Micro QR + MSI + PDF 417 + QR Code + TLC 39 + Trioptic 39 + UK Postal + UPCA + UPCE0 + UPCE1 + US Planet + US Postnet + US 4 State + US 4 State FICS + + Inference (processor) Type + Resolution + Model Input Size + Barcode Symbologies + Detection Parameters + Recognition Parameters + Grouping + Import Database + Export Database + Clear Active Database + Save to Active Database + Similarity Threshold + + Capture image to start + Tap specific products to enroll into active Database + Enrolling into active Database. Please wait + These settings should only be modified by power users + Visit Techdocs for information on the advanced settings > + No products found. Please recapture the image + Camera Start Failed, lower resolution settings or restore defaults + + Allows you to select and load a previously saved database of enrolled products from a file, allowing you to start recognizing products without the need to enroll them. + Saves your current enrolled products as a file to your device’s “Download” directory or SD card, making it easy to backup or share your Active Database. + Removes all of the enrolled products from the Active Database within the application to allow you to start enrolling products from scratch. Note: this does not delete or clear any previously saved database files that have been exported. + + 640 x 640 (small) + 1280 x 1280 (medium) + 1600 x 1600 (large) + 2560 x 2560 (extra large) + + Auto-select + CPU (Central Processing Unit) + GPU (Graphics Processing Unit) + DSP (Digital Signal Processor) + + CPU + GPU + DSP + + 1MP (1280 x 720) + 2MP (1920 x 1080) + 4MP (2688 x 1512) + 8MP (3840 x 2160) + + Select best available option + For trial use if DSP and GPU are not available + For trial use if DSP not available + Best choice (if available) + + Large or close-up barcodes + General barcodes + Dense, faint, or small barcodes + Tiny, distant, or low-contrast barcodes + + Fastest; best for large or close-up barcodes + Balanced speed and accuracy + Slower; use for small, damaged or distant barcodes + More accurate but slower; use for challenging barcodes + + Large or close-up text filling most of the screen + General text + Dense, faint text or detailed documents + Tiny, distant, or low-contrast text + + Fastest; best for large or close-up text + Balanced speed and accuracy + Slower; use for small, damaged or distant text + More accurate but slower; use for challenging fonts + + Heatmap Threshold + Box Threshold + Min Box Area + Min Box Size + Unclip Ratio + Min Ratio for Rotation + + Decoder Type + Cutoff + Top K + Total + + Character Confidence Threshold + Max Word Combinations + TopK Ignore Cutoff + TopK Characters + Total Probability Threshold + + Enable Grouping + Width Distance Ratio + Height Distance Ratio + Center Distance Ratio + Paragraph Height Distance + Paragraph Height Ratio Threshold + + Enable Tiling + Top Correlation Threshold + Merge Points Cutoff + Split Margin Factor + Aspect Ratio Lower Threshold + Aspect Ratio Upper Threshold + TopK Merged Predictions + + Feedback + Audio + Haptic + Show All Detected Barcodes + Beep when filtered text or barcode are found + Vibrate when filtered text or barcode are found + Highlight barcodes that have been detected but not decoded + + Show All + Numeric Characters + Alpha Characters + Alpha Numeric Characters + Exact Match + Starts With + Contains + Regex + + Barcode + OCR (text) + + OCR Filters + Character Type + Character Match + String Length + Regex + Barcode Filters + \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/res/values/themes.xml b/AISuite_Demos/Project 2/Project 2/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..1677006 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +

  • 8ftt4a>N8#M3@=7l^uboW55p0Yqy*)x}sS6q2uGRl66$*2Mi4rZD;- z06Amv^UD(RR0Csgxn74-T3e_or#|F~wMHirtSX8N8mu2l#{M)`k%kmyBu{zml1Ahm z-1Z)59na(abZm6&R#n!xpQRk8*pWBpc37}9>`QLXB7&D)(FVXC{MYT|^MaeP8KNlTXW-ZmGj zdNm%El>SVc-9U?rY_(tEKE8a_dI$+q4ZFCbra8+ScdiKZ=R~fzwv{7zkL=QvJ2}SJBp$UBlW5Hkt;fU+28Q zw6>-0DWDBW-1n$17>0?sR^u{PtmVJVsOXQKFCD@n0CFEdSMLoH+j+s9{xI!~E_Rz} zhFrtDBXAC>4o>3(lP6;IN2AU^lQeFhi%5BI^)zhn?t({A?Uw@593HOTyY{jX9AuvA zvqf{EfP1>CzJJVEsxFenJyOGJ+&rGN+l%6D)RHc3p%TfKGJXmfk35q*=M zI8AG8x-#!>)Vz)``fvzZMxM%k5m&w`*nIlZoXjWj4y8y#w^;-XzG(u~JHwTNoDOUY z)V=WHm(x;v3=AN&n!PUDrW}>j6G3;W6;|O3b~$S}lbX6FAK#eB{44rc#6LJWyl3aO z{l|B9%-q#dG_0C)HBz1|RJt}=pSgl4JcpOiSK9!B(u28F^Muxc{7oLuCnLCV3aLS`jdHMwd?P}Js@{Le5JFOb{9$N{A-aK&T8Q8|S_ ze)y*$4~uWFap*5GxQUS;f73fv^4$=P!lv?~fRlw2_+A=bA@XRa$zyc|!1=k;j}>aa z-HS6q8C_*(>a{=2T`x;x9?ybu5fj@p(9)h%1jPF=vP)K0EaYdUhUF)ZmQF?Nt9xx% z@*>Aw^@Vs@ytn=#B$dAqS4LH)SL`$Gexa_}s7VM{7398*8}H+N`rvAdVI|?@uf4Jt)6kFPI(pjXZR;naoK`mKGi*yjqQ~m1EmVO`b9& z#QSn3-4Nb6NYg9Q8r-}Py;6N+c*MO+5w9?Eclm~6Uy2C5`9N%EFeNIIR}pgML>oH^ zpoCT#6*{?JyApzJ4b;!EGyqp_N>J-9PqQp1wWLsUt_ zzhD=dR0_t7iJFOXWsB+>a$DQ^q2cju+>fHm56$2@hj39{-EB_$BJf1`cFSy-s4l<2 zcDo86G+b0ngDZ_63iJ8@#GD>#0Kxwa1q1-qkem+UTx~`7fPjaycr*U%-*`gGbU$Sg zEC9gVQCYK!bDm0%xq#E*e64ZbDz?n$+(F3hz_cmmC193tRqz~hZWCM<)GBYIv8AX! zxO}x0b&Ml!5rh&Uicr~4sSGKZ)mg0W?m8uk-Z^U<_^4AW z>Nsc62oG2Ug==6`%YlMRD?TixE}Q#*X5?zIqW}Yd#x|A&6O%5xP_wEAWkBx(r>-*z zg(8&FJnFL`t=9RC+y82GbUhXcMl2s*o}XKm-V@@%n1r5p4>rv~mR^nXW4@(xwBtt~ zp*0S54)qO~och=lU?Dw`H(Dbei!u12j@;iZ1qDmo1ya%4(yMFIHXGpz}XX`@WXk9z%wl4^{ zZ6-$xLmkWzAUZ2!F+guZLi36ShFOtg9aZr7itPAjCYqWo}7;3Y-A}U;u;BZVzEni#L^IJ$HcG(0BTuW4_Qo zuIF%!a~Qm4wbmm=+87wYDM2NG`DQxU&;x<)X&eX>d5Em2`NHnQ;Hj00#0Bjg zo{UG)JGT~7<(5R=jYU?l8V07o@x?MKi6i!{OB0d7=`5jPQ7{ZilGp zCN8XlCxS=)cdX$|@_lJ4y0EQXL*7%*2^2^?!teLG@kci8I z*iD6M@MDl#ophinG?qKWV9D02r#`2{3RiL`eklx4DJN + AIDataCaptureDemoApp(viewModel, activityInnerPadding, activityLifecycle) + } + } + } + } + + override fun onDestroy() { + super.onDestroy() + FeedbackUtils.deinitialize() + } + + companion object { + private const val TAG = "MainActivity" + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/PendoInitializer.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/PendoInitializer.kt new file mode 100644 index 0000000..8971acf --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/PendoInitializer.kt @@ -0,0 +1,13 @@ +package com.zebra.aidatacapturedemo + +import android.app.Application +import sdk.pendo.io.Pendo + +object PendoInitializer { + fun init(app: Application, apiKey: String, enabled: Boolean = true) { + if (!enabled) return + Pendo.setup(app, apiKey, null, null) + Pendo.startSession(null, null, null, null) + } +} + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt new file mode 100644 index 0000000..b3fb4a9 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt @@ -0,0 +1,210 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.data + +import android.graphics.Bitmap +import com.zebra.ai.vision.detector.BBox +import com.zebra.aidatacapturedemo.model.FileUtils +import com.zebra.aidatacapturedemo.ui.view.Screen + +/** + * AIDataCaptureDemoUiState.kt is a data class that holds the UI state for the AI Data Capture Demo + */ + +val PROFILING = "Profiling" + +enum class UsecaseState(val value: String) { + Main("None"), + Barcode("Barcode Recognizer"), + BarcodeMap("Barcode Map"), + OCR("Text/OCR Recognizer"), + Retail("Product & Shelf Recognizer"), + OCRBarcodeFind("OCR & Barcode Find"), + Product("Product & Shelf Enrollment") +} + +data class BarcodeSymbology( + var australian_postal :Boolean = false, + var aztec :Boolean = true, + var canadian_postal :Boolean = false, + var chinese_2of5 :Boolean = false, + var codabar :Boolean = true, + var code11 :Boolean = false, + var code39 :Boolean = true, + var code93 :Boolean = false, + var code128 :Boolean = true, + var composite_ab :Boolean = false, + var composite_c :Boolean = false, + var d2of5 :Boolean = false, + var datamatrix :Boolean = true, + var dotcode :Boolean = false, + var dutch_postal :Boolean = false, + var ean_8 :Boolean = true, + var ean_13 :Boolean = true, + var finnish_postal_4s :Boolean = false, + var grid_matrix :Boolean = false, + var gs1_databar :Boolean = true, + var gs1_databar_expanded :Boolean = true, + var gs1_databar_lim :Boolean = false, + var gs1_datamatrix :Boolean = false, + var gs1_qrcode :Boolean = false, + var hanxin :Boolean = false, + var i2of5 :Boolean = false, + var japanese_postal :Boolean = false, + var korean_3of5 :Boolean = false, + var mailmark :Boolean = true, + var matrix_2of5 :Boolean = false, + var maxicode :Boolean = true, + var micropdf :Boolean = false, + var microqr :Boolean = false, + var msi :Boolean = false, + var pdf417 :Boolean = true, + var qrcode :Boolean = true, + var tlc39 :Boolean = false, + var trioptic39 :Boolean = false, + var uk_postal :Boolean = false, + var upc_a :Boolean = true, + var upce0 :Boolean = true, + var upce1 :Boolean = false, + var usplanet :Boolean = false, + var uspostnet :Boolean = false, + var us4state :Boolean = false, + var us4state_fics :Boolean = false, +) + +data class CommonSettings( + var processorSelectedIndex: Int = 0, + var resolutionSelectedIndex: Int = 1, + var inputSizeSelected: Int = 1280, +) + +data class BarcodeSettings( + var commonSettings: CommonSettings = CommonSettings(), + var barcodeSymbology: BarcodeSymbology = BarcodeSymbology() +) { + fun isEquals(other: BarcodeSettings): Boolean { + return commonSettings.processorSelectedIndex == other.commonSettings.processorSelectedIndex && + commonSettings.resolutionSelectedIndex == other.commonSettings.resolutionSelectedIndex && + commonSettings.inputSizeSelected == other.commonSettings.inputSizeSelected && + barcodeSymbology == other.barcodeSymbology + } +} + +data class FeedbackSettings( + var audioBeep: Boolean = true, + var vibration: Boolean = true, + var showDetectedBarcode : Boolean = true +) + +data class OcrBarcodeFindSettings( + var commonSettings: CommonSettings = CommonSettings(), + var barcodeSymbology: BarcodeSymbology = BarcodeSymbology(), + var feedbackSettings: FeedbackSettings = FeedbackSettings() +) + +data class AdvancedOCRSetting( + // Detection Parameters + var heatmapThreshold :String = 0.5f.toString(), + var boxThreshold :String = 0.85f.toString(), + var minBoxArea :String = "10", + var minBoxSize :String = "1", + var unclipRatio :String = 1.5f.toString(), + var minRatioForRotation :String = 1.5f.toString(), + // Recognition Parameters + var maxWordCombinations :String = 10.toString(), + var totalProbabilityThreshold :String = 0.8999f.toString(), + var topkIgnoreCutoff :String = 4.toString(), + + // Tiling Related + var enableTiling : Boolean = false, + var topCorrelationThreshold :String = 0.0f.toString(), + var mergePointsCutoff :String = 5.0f.toString(), + var splitMarginFactor :String = 0.1f.toString(), + var aspectRatioLowerThreshold :String = 10.0f.toString(), + var aspectRatioUpperThreshold :String = 40.toString(), + var topKMergedPredictions :String = 5.0f.toString(), + + //Grouping Related + var enableGrouping : Boolean = false, + var widthDistanceRatio :String = 1.5f.toString(), + var heightDistanceRatio :String = 2.0f.toString(), + var centerDistanceRatio :String = 0.6f.toString(), + var paragraphHeightDistance :String = 1.0f.toString(), + var paragraphHeightRatioThreshold :String = 0.3333f.toString() +) + +data class TextOcrSettings( + var commonSettings: CommonSettings = CommonSettings(), + var advancedOCRSetting: AdvancedOCRSetting = AdvancedOCRSetting() +) + +data class RetailShelfSettings( + var commonSettings: CommonSettings = CommonSettings(), + var similarityThreshold : Float = 80f, +) +data class ProductRecognitionSettings( + var commonSettings: CommonSettings = CommonSettings(), + var similarityThreshold : Float = 80f, +) + +data class OcrBarcodeCaptureSessionData( + var ocrResults: List = listOf(), + var barcodeResults: List = listOf(), + var captureTime: String = "", + var captureImage: String = "" +) + +/** + * AIDataCaptureDemoUiState class used to store UI state data + * This is used to save data from updated by UI as well as Model + */ +data class AIDataCaptureDemoUiState( + // UI --> Model + var usecaseSelected: String = UsecaseState.Main.value, + var activeScreen: Screen = Screen.Start, + var zoomLevel: Float = 1.0f, + val appBarTitle: String = "", + val toastMessage: String? = null, + + // Settings + var barcodeSettings : BarcodeSettings = FileUtils.loadBarcodeSettings(), + var textOCRSettings : TextOcrSettings = FileUtils.loadOCRSettings(), + var ocrBarcodeFindSettings: OcrBarcodeFindSettings = FileUtils.loadOCRBarcodeFindSettings(), + var retailShelfSettings : RetailShelfSettings = FileUtils.loadRetailShelfSettings(), + var productRecognitionSettings: ProductRecognitionSettings = FileUtils.loadProductRecognitionSettings(), + + // Model --> UI + var isOcrModelDemoReady: Boolean = false, + var isBarcodeModelDemoReady: Boolean = false, + var isRetailShelfModelDemoReady: Boolean = false, + var isCameraReady: Boolean = false, + var cameraError: String? = null, + var isProductEnrollmentCompleted: Boolean = false, + var currentBitmap: Bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888), + var captureBitmap: Bitmap? = null, + var bboxes: Array = arrayOf(), + var moduleResults: ModuleData = ModuleData(mutableListOf(), mutableListOf(), mutableListOf()), + var productResults: MutableList = mutableListOf(), + val ocrResults: List = listOf(), + var barcodeResults: List = listOf(), + var selectedToteId: String? = null, + var allCustomers: List = listOf(), + var selectedCustomer: CustomerInfo? = null, + var pickingFeedback: String? = null, + var lastScannedProduct: ProductInfo? = null, + var targetTotes: List> = listOf(), // Tote ID to Quantity + + // Choices + var isBarcodeModelEnabled: Boolean = true, + var isOCRModelEnabled: Boolean = true, + var isCaptureOrLiveEnabled: Int = 0, // 0 for Capture, 1 for Live + var allBarcodeOCRCaptureFilter: Int = 0, // 0 for All, 1 for Barcode, 2 for OCR + + var ocrBarcodeCaptureSessionCount : Int = 0, + var ocrBarcodeCaptureSessionIndex : Int = 0, + + var selectedFilterType: FilterType = FilterType.NONE, + var ocrFilterData: OcrFilterData = FileUtils.loadOcrFilterData(), + var barcodeFilterData: BarcodeFilterData = FileUtils.loadBarcodeFilterData() +) + diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/FilterData.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/FilterData.kt new file mode 100644 index 0000000..94d39d3 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/FilterData.kt @@ -0,0 +1,83 @@ +package com.zebra.aidatacapturedemo.data + +/* * FilterData.kt is a data class that defines the structure for filter settings used in the +OCR-Barcode Find Usecase Demo. It includes various enums and data classes to represent +different filter options for OCR and barcode recognition. The filter settings allow users to +customize the detection process by applying regular expressions, character type filters, +character match filters, and string length filters. +This structured approach helps manage the complexity of filter configurations and provides a +clear way to store and access filter-related data. + */ +enum class FilterType(val value: String) { + NONE(value = "None"), + OCR_FILTER(value = "OcrFilter"), + BARCODE_FILTER(value = "BarcodeFilter") +} + +enum class OcrRegularFilterOption { + UNFILTERED, + REGEX, + ADVANCED +} + +enum class AdvancedFilterOption { + CHARACTER_TYPE, + CHARACTER_MATCH, + STRING_LENGTH +} + +enum class CharacterTypeFilterOption { + SELECT_ALL, + ALPHA, + NUMERIC, + INCLUDE_SPECIAL_CHARACTERS +} + +enum class CharacterMatchFilterOption { + STARTS_WITH, + CONTAINS, + EXACT_MATCH +} + +enum class DetectionLevel { + WORD, + LINE +} + +data class RegexData( + var detectionLevel: DetectionLevel = DefaultValues.OCR_DETECTION_LEVEL, + var regexAdditionalStringList: MutableList = mutableListOf(), + var regexDefaultString: String = DefaultValues.DEFAULT_REGEX_STRING +) + +data class CharacterMatchData( + var type: CharacterMatchFilterOption = DefaultValues.CHARACTER_MATCH_FILTER_OPTION, + var detectionLevel: DetectionLevel = DefaultValues.OCR_DETECTION_LEVEL, + var startsWithStringList: List = listOf(), + var containsStringList: List = listOf(), + var exactMatchStringList: List = listOf() +) + +data class OcrFilterData( + var selectedRegularFilterOption: OcrRegularFilterOption = DefaultValues.OCR_REGULAR_FILTER_OPTION, + var selectedAdvancedFilterOptionList: MutableList = mutableListOf(), + var selectedRegexFilterData: RegexData = RegexData(), + var selectedCharacterTypeFilterOptionList: MutableList = mutableListOf(), + var selectedCharacterMatchFilterData: CharacterMatchData = CharacterMatchData(), + var selectedStringLengthRange: ClosedFloatingPointRange = (DefaultValues.STRING_LENGTH_RANGE_MIN_VALUE ..DefaultValues.STRING_LENGTH_RANGE_MAX_VALUE) +) + +data class BarcodeFilterData( + var selectedAdvancedFilterOptionList: MutableList = mutableListOf(), + var selectedCharacterMatchFilterData: CharacterMatchData = CharacterMatchData(), + var selectedStringLengthRange: ClosedFloatingPointRange = (DefaultValues.STRING_LENGTH_RANGE_MIN_VALUE ..DefaultValues.STRING_LENGTH_RANGE_MAX_VALUE) +) + +object DefaultValues { + val OCR_REGULAR_FILTER_OPTION = OcrRegularFilterOption.UNFILTERED + val CHARACTER_MATCH_FILTER_OPTION = CharacterMatchFilterOption.STARTS_WITH + val OCR_DETECTION_LEVEL = DetectionLevel.WORD + const val STRING_LENGTH_RANGE_MIN_VALUE = 2f + const val STRING_LENGTH_RANGE_MAX_VALUE = 15f + const val DEFAULT_REGEX_STRING = "" +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/ModuleData.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/ModuleData.kt new file mode 100644 index 0000000..fc9c606 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/ModuleData.kt @@ -0,0 +1,16 @@ +package com.zebra.aidatacapturedemo.data + +import android.graphics.Bitmap +import com.zebra.ai.vision.detector.BBox +import com.zebra.ai.vision.entity.LabelEntity +import com.zebra.ai.vision.entity.ProductEntity +import com.zebra.ai.vision.entity.ShelfEntity + +/** + * ModuleData.kt is a data class that encapsulates the results from the Product & Shelf Recognizer + * module. It contains lists of ShelfEntity, LabelEntity, and ProductEntity objects, which represent + * the detected shelves, labels, and products in the input image. This class serves as a structured + * way to store and access the results from the recognition process, allowing for easy integration + * with the UI and other components of the application. + */ +class ModuleData(var shelves: List, var labelEntity: List, var productEntity: List ) \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/ProductData.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/ProductData.kt new file mode 100644 index 0000000..e6fda96 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/ProductData.kt @@ -0,0 +1,61 @@ +package com.zebra.aidatacapturedemo.data + +import android.graphics.Bitmap +import android.graphics.Point +import android.util.Log +import com.zebra.ai.vision.detector.BBox +import com.zebra.ai.vision.detector.Recognizer.Recognition + +/** + * ProductData class used to store product recognition data + * @param point: Point + * @param text: String + * @param bBox: BBox + * @param crop: Bitmap + */ +class ProductData(var text: String, var bBox: BBox, var crop : Bitmap) + +/** + * toProductData function used to convert input bitmap, products and recognitions to product data + * @param inputBitmap: Bitmap + * @param products: Array + * @param recognitions: Array + * @return MutableList + */ +fun toProductData(similarityThreshold : Float, inputBitmap:Bitmap, products: Array, recognitions: Array): MutableList { + val ProductData = mutableListOf() + for (i in products.indices) { + if((products[i].xmin.toInt() + (products[i].xmax - products[i].xmin).toInt() < inputBitmap.width) && + (products[i].ymin.toInt() + (products[i].ymax - products[i].ymin).toInt() < inputBitmap.height)) { + if (recognitions[i].similarity.first() > similarityThreshold) { + ProductData += ProductData( + recognitions[i].sku.first(), + products[i], + Bitmap.createBitmap( + inputBitmap, + products[i].xmin.toInt(), + products[i].ymin.toInt(), + (products[i].xmax - products[i].xmin).toInt(), + (products[i].ymax - products[i].ymin).toInt() + ) + ) + } else { + ProductData += ProductData( + "", + products[i], + Bitmap.createBitmap( + inputBitmap, + products[i].xmin.toInt(), + products[i].ymin.toInt(), + (products[i].xmax - products[i].xmin).toInt(), + (products[i].ymax - products[i].ymin).toInt() + ) + ) + } + } + else { + Log.i("ProductData", "Product BBox out of bounds") + } + } + return ProductData +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/ResultData.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/ResultData.kt new file mode 100644 index 0000000..195912e --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/ResultData.kt @@ -0,0 +1,10 @@ +package com.zebra.aidatacapturedemo.data + +import android.graphics.Rect + +/** + * ResultData class used to store OCR-Barcode Find results + * @param boundingBox: Rect + * @param text: String + */ +class ResultData(var boundingBox: Rect, var text: String) \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/BarcodeAnalyzer.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/BarcodeAnalyzer.kt new file mode 100644 index 0000000..8f9e997 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/BarcodeAnalyzer.kt @@ -0,0 +1,279 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.model + +import android.graphics.Bitmap +import android.util.Log +import androidx.lifecycle.Lifecycle +import com.zebra.ai.vision.detector.AIVisionSDKException +import com.zebra.ai.vision.detector.BarcodeDecoder +import com.zebra.ai.vision.detector.ImageData +import com.zebra.ai.vision.detector.InvalidInputException +import com.zebra.ai.vision.entity.BarcodeEntity +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.PROFILING +import com.zebra.aidatacapturedemo.data.ResultData +import com.zebra.aidatacapturedemo.data.UsecaseState +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import kotlinx.coroutines.flow.StateFlow +import java.io.IOException +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + + +/** + * [BarcodeAnalyzer] class is used to detect & Track barcodes found on the Camera Live Preview + * + * @param uiState - Used to read all the UI Current State + * @param viewModel - Used to write any UI State Changes via [AIDataCaptureDemoViewModel] + */ +class BarcodeAnalyzer( + val uiState: StateFlow, + val viewModel: AIDataCaptureDemoViewModel) { + + private lateinit var mActivityLifecycle: Lifecycle + private val TAG = "BarcodeAnalyzer" + private var barcodeDecoder: BarcodeDecoder? = null + private val decoderSettings = BarcodeDecoder.Settings("barcode-localizer") + private val executorService: ExecutorService = Executors.newSingleThreadExecutor() + + /** + * initialize function is used to initialize the BarcodeDecoder for Barcode Analyzer + * use case. It configures the model settings based on the current UI state and creates an + * instance of BarcodeDecoder. If the initialization fails due to unsupported inference type, + * it updates the UI with appropriate messages and takes corrective actions. + */ + fun initialize() { + barcodeDecoder?.dispose() + barcodeDecoder = null + updateBarcodeModelDemoReady(false) + try { + configure() + + val mStart = System.currentTimeMillis() + BarcodeDecoder.getBarcodeDecoder(decoderSettings, executorService) + .thenAccept { barcodeDecoderInstance: BarcodeDecoder -> + barcodeDecoder = barcodeDecoderInstance + updateBarcodeModelDemoReady(true) + Log.e( + PROFILING, + "BarcodeAnalyzer obj creation / model loading time = ${System.currentTimeMillis() - mStart} milli sec" + ) + Log.i(TAG, "BarcodeAnalyzer init Success") + }.exceptionally { e: Throwable -> + Log.e(TAG, "BarcodeAnalyzer init Failed -> " + e.message) + if (e.message?.contains("Given runtimes are not available") == true || + e.message?.contains("Initialize barcodeDecoder due to SNPE exception") == true + ) { + viewModel.updateToastMessage(message = "Selected inference type is not supported on this device. Switching to Auto-select for optimal performance.") + viewModel.updateSelectedProcessor(0) //Auto-Select + viewModel.saveSettings() + initialize() + } + null + } + } catch (ex: IOException) { + Log.e(TAG, "getBarcodeDecoder init Failed -> " + ex.message) + } + } + + /** + * To deinitialize the BarcodeAnalyzer, we need to dispose the localizer + */ + fun deinitialize() { + barcodeDecoder?.dispose() + barcodeDecoder = null + } + fun getDetector() : BarcodeDecoder? { + return barcodeDecoder + } + + /** executeHighRes function takes a high resolution bitmap as input and processes it using the + * BarcodeDecoder instance. It runs the processing in a background thread using an ExecutorService. + * The function handles exceptions that may occur during processing and logs appropriate messages. + * Upon successful processing, it calls the onDetectionBarcodeResult function with the results. + */ + fun executeHighRes(highResBitmap: Bitmap) { + executorService.submit { + try { + Log.d(TAG, "Starting image analysis") + val highResImageData: ImageData = ImageData.fromBitmap(highResBitmap, 0) + barcodeDecoder?.process(highResImageData) + ?.thenAccept { result -> + onDetectionBarcodeResult(result) + } + } catch (e: InvalidInputException) { + Log.e(TAG, e.message ?: "InvalidInputException occurred") + } catch (e: AIVisionSDKException) { + Log.e(TAG, e.message ?: "AIVisionSDKException occurred") + } finally { + } + } + } + private fun configure() { + try { + if (uiState.value.usecaseSelected == UsecaseState.OCRBarcodeFind.value) { + //Swap the values as the presented index is reverse of what model expects + val processorOrder = + when (uiState.value.ocrBarcodeFindSettings.commonSettings.processorSelectedIndex) { + 0 -> arrayOf(2, 0, 1) // AUTO + 1 -> arrayOf(2) // DSP + 2 -> arrayOf(1) // GPU + 3 -> arrayOf(0) //CPU + else -> { + arrayOf(2, 0, 1) + } + } + decoderSettings.detectorSetting.inferencerOptions.runtimeProcessorOrder = + processorOrder + + decoderSettings.detectorSetting.inferencerOptions.defaultDims.width = + uiState.value.ocrBarcodeFindSettings.commonSettings.inputSizeSelected + decoderSettings.detectorSetting.inferencerOptions.defaultDims.height = + uiState.value.ocrBarcodeFindSettings.commonSettings.inputSizeSelected + + decoderSettings.Symbology.AUSTRALIAN_POSTAL.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.australian_postal) + decoderSettings.Symbology.AZTEC.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.aztec) + decoderSettings.Symbology.CANADIAN_POSTAL.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.canadian_postal) + decoderSettings.Symbology.CHINESE_2OF5.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.chinese_2of5) + decoderSettings.Symbology.CODABAR.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.codabar) + decoderSettings.Symbology.CODE11.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.code11) + decoderSettings.Symbology.CODE39.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.code39) + decoderSettings.Symbology.CODE93.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.code93) + decoderSettings.Symbology.CODE128.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.code128) + decoderSettings.Symbology.COMPOSITE_AB.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.composite_ab) + decoderSettings.Symbology.COMPOSITE_C.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.composite_c) + decoderSettings.Symbology.D2OF5.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.d2of5) + decoderSettings.Symbology.DATAMATRIX.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.datamatrix) + decoderSettings.Symbology.DOTCODE.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.dotcode) + decoderSettings.Symbology.DUTCH_POSTAL.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.dutch_postal) + decoderSettings.Symbology.EAN8.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.ean_8) + decoderSettings.Symbology.EAN13.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.ean_13) + decoderSettings.Symbology.FINNISH_POSTAL_4S.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.finnish_postal_4s) + decoderSettings.Symbology.GRID_MATRIX.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.grid_matrix) + decoderSettings.Symbology.GS1_DATABAR.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.gs1_databar) + decoderSettings.Symbology.GS1_DATABAR_EXPANDED.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.gs1_databar_expanded) + decoderSettings.Symbology.GS1_DATABAR_LIM.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.gs1_databar_lim) + decoderSettings.Symbology.GS1_DATAMATRIX.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.gs1_datamatrix) + decoderSettings.Symbology.GS1_QRCODE.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.gs1_qrcode) + decoderSettings.Symbology.HANXIN.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.hanxin) + decoderSettings.Symbology.I2OF5.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.i2of5) + decoderSettings.Symbology.JAPANESE_POSTAL.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.japanese_postal) + decoderSettings.Symbology.KOREAN_3OF5.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.korean_3of5) + decoderSettings.Symbology.MAILMARK.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.mailmark) + decoderSettings.Symbology.MATRIX_2OF5.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.matrix_2of5) + decoderSettings.Symbology.MAXICODE.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.maxicode) + decoderSettings.Symbology.MICROPDF.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.micropdf) + decoderSettings.Symbology.MICROQR.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.microqr) + decoderSettings.Symbology.MSI.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.msi) + decoderSettings.Symbology.PDF417.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.pdf417) + decoderSettings.Symbology.QRCODE.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.qrcode) + decoderSettings.Symbology.TLC39.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.tlc39) + decoderSettings.Symbology.TRIOPTIC39.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.trioptic39) + decoderSettings.Symbology.UK_POSTAL.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.uk_postal) + decoderSettings.Symbology.UPCA.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.upc_a) + decoderSettings.Symbology.UPCE0.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.upce0) + decoderSettings.Symbology.UPCE1.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.upce1) + decoderSettings.Symbology.USPLANET.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.usplanet) + decoderSettings.Symbology.USPOSTNET.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.uspostnet) + decoderSettings.Symbology.US4STATE.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.us4state) + decoderSettings.Symbology.US4STATE_FICS.enable(uiState.value.ocrBarcodeFindSettings.barcodeSymbology.us4state_fics) + } else { + //Swap the values as the presented index is reverse of what model expects + val processorOrder = + when (uiState.value.barcodeSettings.commonSettings.processorSelectedIndex) { + 0 -> arrayOf(2, 0, 1) // AUTO + 1 -> arrayOf(2) // DSP + 2 -> arrayOf(1) // GPU + 3 -> arrayOf(0) //CPU + else -> { + arrayOf(2, 0, 1) + } + } + decoderSettings.detectorSetting.inferencerOptions.runtimeProcessorOrder = + processorOrder + + decoderSettings.detectorSetting.inferencerOptions.defaultDims.width = + uiState.value.barcodeSettings.commonSettings.inputSizeSelected + decoderSettings.detectorSetting.inferencerOptions.defaultDims.height = + uiState.value.barcodeSettings.commonSettings.inputSizeSelected + + decoderSettings.Symbology.AUSTRALIAN_POSTAL.enable(uiState.value.barcodeSettings.barcodeSymbology.australian_postal) + decoderSettings.Symbology.AZTEC.enable(uiState.value.barcodeSettings.barcodeSymbology.aztec) + decoderSettings.Symbology.CANADIAN_POSTAL.enable(uiState.value.barcodeSettings.barcodeSymbology.canadian_postal) + decoderSettings.Symbology.CHINESE_2OF5.enable(uiState.value.barcodeSettings.barcodeSymbology.chinese_2of5) + decoderSettings.Symbology.CODABAR.enable(uiState.value.barcodeSettings.barcodeSymbology.codabar) + decoderSettings.Symbology.CODE11.enable(uiState.value.barcodeSettings.barcodeSymbology.code11) + decoderSettings.Symbology.CODE39.enable(uiState.value.barcodeSettings.barcodeSymbology.code39) + decoderSettings.Symbology.CODE93.enable(uiState.value.barcodeSettings.barcodeSymbology.code93) + decoderSettings.Symbology.CODE128.enable(uiState.value.barcodeSettings.barcodeSymbology.code128) + decoderSettings.Symbology.COMPOSITE_AB.enable(uiState.value.barcodeSettings.barcodeSymbology.composite_ab) + decoderSettings.Symbology.COMPOSITE_C.enable(uiState.value.barcodeSettings.barcodeSymbology.composite_c) + decoderSettings.Symbology.D2OF5.enable(uiState.value.barcodeSettings.barcodeSymbology.d2of5) + decoderSettings.Symbology.DATAMATRIX.enable(uiState.value.barcodeSettings.barcodeSymbology.datamatrix) + decoderSettings.Symbology.DOTCODE.enable(uiState.value.barcodeSettings.barcodeSymbology.dotcode) + decoderSettings.Symbology.DUTCH_POSTAL.enable(uiState.value.barcodeSettings.barcodeSymbology.dutch_postal) + decoderSettings.Symbology.EAN8.enable(uiState.value.barcodeSettings.barcodeSymbology.ean_8) + decoderSettings.Symbology.EAN13.enable(uiState.value.barcodeSettings.barcodeSymbology.ean_13) + decoderSettings.Symbology.FINNISH_POSTAL_4S.enable(uiState.value.barcodeSettings.barcodeSymbology.finnish_postal_4s) + decoderSettings.Symbology.GRID_MATRIX.enable(uiState.value.barcodeSettings.barcodeSymbology.grid_matrix) + decoderSettings.Symbology.GS1_DATABAR.enable(uiState.value.barcodeSettings.barcodeSymbology.gs1_databar) + decoderSettings.Symbology.GS1_DATABAR_EXPANDED.enable(uiState.value.barcodeSettings.barcodeSymbology.gs1_databar_expanded) + decoderSettings.Symbology.GS1_DATABAR_LIM.enable(uiState.value.barcodeSettings.barcodeSymbology.gs1_databar_lim) + decoderSettings.Symbology.GS1_DATAMATRIX.enable(uiState.value.barcodeSettings.barcodeSymbology.gs1_datamatrix) + decoderSettings.Symbology.GS1_QRCODE.enable(uiState.value.barcodeSettings.barcodeSymbology.gs1_qrcode) + decoderSettings.Symbology.HANXIN.enable(uiState.value.barcodeSettings.barcodeSymbology.hanxin) + decoderSettings.Symbology.I2OF5.enable(uiState.value.barcodeSettings.barcodeSymbology.i2of5) + decoderSettings.Symbology.JAPANESE_POSTAL.enable(uiState.value.barcodeSettings.barcodeSymbology.japanese_postal) + decoderSettings.Symbology.KOREAN_3OF5.enable(uiState.value.barcodeSettings.barcodeSymbology.korean_3of5) + decoderSettings.Symbology.MAILMARK.enable(uiState.value.barcodeSettings.barcodeSymbology.mailmark) + decoderSettings.Symbology.MATRIX_2OF5.enable(uiState.value.barcodeSettings.barcodeSymbology.matrix_2of5) + decoderSettings.Symbology.MAXICODE.enable(uiState.value.barcodeSettings.barcodeSymbology.maxicode) + decoderSettings.Symbology.MICROPDF.enable(uiState.value.barcodeSettings.barcodeSymbology.micropdf) + decoderSettings.Symbology.MICROQR.enable(uiState.value.barcodeSettings.barcodeSymbology.microqr) + decoderSettings.Symbology.MSI.enable(uiState.value.barcodeSettings.barcodeSymbology.msi) + decoderSettings.Symbology.PDF417.enable(uiState.value.barcodeSettings.barcodeSymbology.pdf417) + decoderSettings.Symbology.QRCODE.enable(uiState.value.barcodeSettings.barcodeSymbology.qrcode) + decoderSettings.Symbology.TLC39.enable(uiState.value.barcodeSettings.barcodeSymbology.tlc39) + decoderSettings.Symbology.TRIOPTIC39.enable(uiState.value.barcodeSettings.barcodeSymbology.trioptic39) + decoderSettings.Symbology.UK_POSTAL.enable(uiState.value.barcodeSettings.barcodeSymbology.uk_postal) + decoderSettings.Symbology.UPCA.enable(uiState.value.barcodeSettings.barcodeSymbology.upc_a) + decoderSettings.Symbology.UPCE0.enable(uiState.value.barcodeSettings.barcodeSymbology.upce0) + decoderSettings.Symbology.UPCE1.enable(uiState.value.barcodeSettings.barcodeSymbology.upce1) + decoderSettings.Symbology.USPLANET.enable(uiState.value.barcodeSettings.barcodeSymbology.usplanet) + decoderSettings.Symbology.USPOSTNET.enable(uiState.value.barcodeSettings.barcodeSymbology.uspostnet) + decoderSettings.Symbology.US4STATE.enable(uiState.value.barcodeSettings.barcodeSymbology.us4state) + decoderSettings.Symbology.US4STATE_FICS.enable(uiState.value.barcodeSettings.barcodeSymbology.us4state_fics) + } + } catch (e: Exception) { + Log.e(TAG, "Fatal error: configure failed - ${e.message}") + } + } + + private fun updateBarcodeModelDemoReady(isReady: Boolean) { + viewModel.updateBarcodeModelDemoReady(isReady = isReady) + } + + private fun onDetectionBarcodeResult(entityList: List?) { + var rectList: MutableList = mutableListOf() + entityList?.forEach { entity -> + if (entity != null) { + val value = entity.value + val rect = entity.boundingBox + rectList += ResultData(boundingBox = rect, text = value) + } + } + + // If feedbackSettings.showDetectedBarcode is false -> then don't show the undecoded barcodes on the display + if (!uiState.value.ocrBarcodeFindSettings.feedbackSettings.showDetectedBarcode){ + rectList.retainAll { it.text.isNotBlank() } + } + + viewModel.updateBarcodeResultData( + results = FilterUtils.getBarcodeFilteredResultData( + uiState = uiState.value, + outputBarcodeResultData = rectList + ) + ) + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/CustomClosedFloatingPointRangeAdapter.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/CustomClosedFloatingPointRangeAdapter.kt new file mode 100644 index 0000000..6f4e520 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/CustomClosedFloatingPointRangeAdapter.kt @@ -0,0 +1,38 @@ +package com.zebra.aidatacapturedemo.model + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import java.lang.reflect.Type + +// Gson lib cannot directly typecase kotlin.ranges.ClosedFloatingPointRange and store under file, +// hence use the include the following Adapter Class explicitly for Gson +class CustomClosedFloatingPointRangeAdapter : JsonSerializer>, + JsonDeserializer> { + override fun serialize( + src: ClosedFloatingPointRange<*>, + typeOfSrc: Type?, + context: JsonSerializationContext + ): JsonElement { + // Serialize the range to a simple JSON object, e.g., {"start": 0.0, "endInclusive": 10.0} + val obj = JsonObject() + obj.addProperty("start", src.start as Number) + obj.addProperty("endInclusive", src.endInclusive as Number) + return obj + } + + override fun deserialize( + json: JsonElement, + typeOfT: Type?, + context: JsonDeserializationContext + ): ClosedFloatingPointRange<*> { + val obj = json.asJsonObject + val start = obj.get("start").asDouble + val endInclusive = obj.get("endInclusive").asDouble + // Reconstruct the concrete range type + return start..endInclusive + } +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/FileUtils.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/FileUtils.kt new file mode 100644 index 0000000..28912a5 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/FileUtils.kt @@ -0,0 +1,454 @@ +package com.zebra.aidatacapturedemo.model + +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.zebra.ai.vision.detector.BBox +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.BarcodeFilterData +import com.zebra.aidatacapturedemo.data.BarcodeSettings +import com.zebra.aidatacapturedemo.data.FilterType +import com.zebra.aidatacapturedemo.data.OcrBarcodeCaptureSessionData +import com.zebra.aidatacapturedemo.data.OcrBarcodeFindSettings +import com.zebra.aidatacapturedemo.data.OcrFilterData +import com.zebra.aidatacapturedemo.data.ProductData +import com.zebra.aidatacapturedemo.data.ProductRecognitionSettings +import com.zebra.aidatacapturedemo.data.ResultData +import com.zebra.aidatacapturedemo.data.RetailShelfSettings +import com.zebra.aidatacapturedemo.data.TextOcrSettings +import com.zebra.aidatacapturedemo.data.UsecaseState +import java.io.File +import java.io.FileOutputStream +import java.io.FileWriter +import java.io.OutputStream +import java.nio.file.Paths +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.ArrayDeque + + +/** + * FileUtils class to provide utility functions related to filesystem + */ +class FileUtils(cacheDir: String, context : Context) { + init { + mCacheDir = cacheDir + mContext = context + barcodeSettingsFile = File(mCacheDir, "barcode_settings.json") + ocrTextSettingsFile = File(mCacheDir, "ocr_text_settings.json") + retailShelfSettingsFile = File(mCacheDir, "retailshelf_settings.json") + ocrBarcodeFindSettingsFile = File(mCacheDir, "ocrbarcodefind_settings.json") + productRecogntionSettingsFile= File(mCacheDir, "product_recognition_settings.json") + ocrFilterDataFile = File(mCacheDir, "ocr_filter_data.json") + barcodeFilterDataFile = File(mCacheDir, "barcode_filter_data.json") + settingsFiles.put(UsecaseState.Barcode.value, barcodeSettingsFile) + settingsFiles.put(UsecaseState.OCR.value, ocrTextSettingsFile) + settingsFiles.put(UsecaseState.OCRBarcodeFind.value, ocrBarcodeFindSettingsFile) + settingsFiles.put(UsecaseState.Retail.value, retailShelfSettingsFile) + settingsFiles.put(UsecaseState.Product.value, productRecogntionSettingsFile) + settingsFiles.put(FilterType.OCR_FILTER.value, ocrFilterDataFile) + settingsFiles.put(FilterType.BARCODE_FILTER.value, barcodeFilterDataFile) + } + + companion object { + private val TAG: String = "FileUtils" + lateinit var mCacheDir: String + lateinit var mContext : Context + lateinit var mSavedTimeStamp : String + var databaseFile: String = "products.db" + private val gson = GsonBuilder() + .registerTypeAdapter(ClosedFloatingPointRange::class.java, CustomClosedFloatingPointRangeAdapter()) + .create() + + private lateinit var barcodeSettingsFile: File + private lateinit var ocrTextSettingsFile: File + private lateinit var retailShelfSettingsFile: File + private lateinit var ocrBarcodeFindSettingsFile: File + private lateinit var productRecogntionSettingsFile: File + private lateinit var ocrFilterDataFile: File + private lateinit var barcodeFilterDataFile: File + + var settingsFiles : MutableMap = mutableMapOf() + + fun loadBarcodeSettings(): BarcodeSettings { + + return if (settingsFiles.getValue(UsecaseState.Barcode.value).exists()) { + try { + val json = settingsFiles.getValue(UsecaseState.Barcode.value).readText() + gson.fromJson(json, BarcodeSettings::class.java) ?: BarcodeSettings() + } catch (_: Exception) { + BarcodeSettings() + } + } else { + BarcodeSettings() + } + } + + fun saveBarcodeSettings(settings: BarcodeSettings) { + try { + FileWriter(settingsFiles.getValue(UsecaseState.Barcode.value)).use { writer -> + gson.toJson(settings, writer) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + fun loadOCRSettings(): TextOcrSettings { + return if (settingsFiles.getValue(UsecaseState.OCR.value).exists()) { + try { + val json = settingsFiles.getValue(UsecaseState.OCR.value).readText() + gson.fromJson(json, TextOcrSettings::class.java) ?: TextOcrSettings() + } catch (_: Exception) { + TextOcrSettings() + } + } else { + TextOcrSettings() + } + } + + fun saveOCRSettings(settings: TextOcrSettings) { + try { + FileWriter(settingsFiles.getValue(UsecaseState.OCR.value)).use { writer -> + gson.toJson(settings, writer) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun loadOCRBarcodeFindSettings(): OcrBarcodeFindSettings { + return if (settingsFiles.getValue(UsecaseState.OCRBarcodeFind.value).exists()) { + try { + val json = settingsFiles.getValue(UsecaseState.OCRBarcodeFind.value).readText() + gson.fromJson(json, OcrBarcodeFindSettings::class.java) ?: OcrBarcodeFindSettings() + } catch (_: Exception) { + OcrBarcodeFindSettings() + } + } else { + OcrBarcodeFindSettings() + } + } + + fun saveOCRBarcodeFindSettings(settings: OcrBarcodeFindSettings) { + try { + FileWriter(settingsFiles.getValue(UsecaseState.OCRBarcodeFind.value)).use { writer -> + gson.toJson(settings, writer) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun loadRetailShelfSettings(): RetailShelfSettings { + return if (settingsFiles.getValue(UsecaseState.Retail.value).exists()) { + try { + val json = settingsFiles.getValue(UsecaseState.Retail.value).readText() + gson.fromJson(json, RetailShelfSettings::class.java) ?: RetailShelfSettings() + } catch (_: Exception) { + RetailShelfSettings() + } + } else { + RetailShelfSettings() + } + } + + fun saveRetailShelfSettings(settings: RetailShelfSettings) { + try { + FileWriter(settingsFiles.getValue(UsecaseState.Retail.value)).use { writer -> + gson.toJson(settings, writer) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun loadProductRecognitionSettings(): ProductRecognitionSettings { + return if (settingsFiles.getValue(UsecaseState.Product.value).exists()) { + try { + val json = settingsFiles.getValue(UsecaseState.Product.value).readText() + gson.fromJson(json, ProductRecognitionSettings::class.java) ?: ProductRecognitionSettings() + } catch (_: Exception) { + ProductRecognitionSettings() + } + } else { + ProductRecognitionSettings() + } + } + + fun saveOcrFilterData(ocrFilterData: OcrFilterData) { + try { + FileWriter(settingsFiles.getValue(FilterType.OCR_FILTER.value)).use { writer -> + gson.toJson(ocrFilterData, writer) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun loadOcrFilterData(): OcrFilterData { + return if (settingsFiles.getValue(FilterType.OCR_FILTER.value).exists()) { + try { + val json = settingsFiles.getValue(FilterType.OCR_FILTER.value).readText() + gson.fromJson(json, OcrFilterData::class.java) ?: OcrFilterData() + } catch (_: Exception) { + OcrFilterData() + } + } else { + OcrFilterData() + } + } + + fun saveBarcodeFilterData(barcodeFilterData: BarcodeFilterData) { + try { + FileWriter(settingsFiles.getValue(FilterType.BARCODE_FILTER.value)).use { writer -> + gson.toJson(barcodeFilterData, writer) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun loadBarcodeFilterData(): BarcodeFilterData { + return if (settingsFiles.getValue(FilterType.BARCODE_FILTER.value).exists()) { + try { + val json = settingsFiles.getValue(FilterType.BARCODE_FILTER.value).readText() + gson.fromJson(json, BarcodeFilterData::class.java) ?: BarcodeFilterData() + } catch (_: Exception) { + BarcodeFilterData() + } + } else { + BarcodeFilterData() + } + } + + fun saveProductRecognitionSettings(settings: ProductRecognitionSettings) { + try { + FileWriter(settingsFiles.getValue(UsecaseState.Product.value)).use { writer -> + gson.toJson(settings, writer) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun saveBarcodeResultsToFile(barcodeResults: List) { + try { + val timestamp = getTimeStamp() + val fileName = "barcode_layout_$timestamp.json" + val file = File(mContext.getExternalFilesDir(null), fileName) + FileWriter(file).use { writer -> + gson.toJson(barcodeResults, writer) + } + Log.d(TAG, "Barcode results saved to ${file.absolutePath}") + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun getTimeStamp(): String { + return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmssSSS")) + } + + /** + * This function is used to create a timestamped folder in the external files directory to + * save product images + */ + fun getTimeStampedFolderName() : String { + mSavedTimeStamp = getTimeStamp() + val timestampFolder = File(mContext.getExternalFilesDir(null), mSavedTimeStamp) + if (!timestampFolder.exists()) { + timestampFolder.mkdir() + } + return mSavedTimeStamp + } + + /** + * This function is used to save the bitmap image in the Pictures folder + */ + fun saveBitmap(bmp: Bitmap, + subFolderName: String?, + filename: String?) { + var imageOutStream: OutputStream + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val values = ContentValues() + values.put(MediaStore.Images.Media.DISPLAY_NAME, filename); + values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); + values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/" +subFolderName); + + val uri = mContext.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) + imageOutStream = uri?.let { mContext.getContentResolver().openOutputStream(it) }!! + } else { + val folder = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString(), subFolderName) + if (!folder.exists()) { + folder.mkdir() + } + val file = File(folder, filename) + imageOutStream = FileOutputStream(file); + } + bmp.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream); + imageOutStream.flush(); + imageOutStream.close(); + } + + + /** + * This function is used to delete the products.db file from the cache directory + */ + fun deleteProductDBFile() { + val path = Paths.get(mCacheDir, databaseFile).toString() + val file = File(path) + file.delete() + } + + /** + * This function is used to save the product database file (products.db) in Downloads folder + */ + fun saveProductDBFile() { + val productDBFile = File(mCacheDir, databaseFile) + val contentValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, "products.db") + put(MediaStore.MediaColumns.MIME_TYPE, "*/*") + put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } + // Query for the file + val cursor: Cursor? = mContext.getContentResolver().query(MediaStore.Downloads.EXTERNAL_CONTENT_URI, null, null, null, null) + var fileUri: Uri? = null + // If file found + if (cursor != null && cursor.count > 0) { + // Get URI + while (cursor.moveToNext()) { + val nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) + if (nameIndex > -1) { + val displayName = cursor.getString(nameIndex) + if (displayName == "products.db") { + val idIndex = cursor.getColumnIndex(MediaStore.MediaColumns._ID) + if (idIndex > -1) { + val id = cursor.getLong(idIndex) + fileUri = ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id) + break + } + } + } + } + cursor.close() + } else { + // insert new file otherwise + val resolver = mContext.contentResolver + fileUri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) + } + saveFile(productDBFile.toUri(), fileUri) + } + + /** + * This function is used to reads product crops in Downloads folder + */ + fun readProductCrops(uri: Uri) : List{ + + // Creates a remembered mutable state list to store paths of images. + val rootDirectoryFile = DocumentFile.fromTreeUri(mContext, uri) + val directories = ArrayDeque(listOf(rootDirectoryFile)) + val imageFileUris = mutableListOf() + val listOfProductData = mutableListOf() + + // Loop through all of the subdirectories, starting with the root + while (directories.isNotEmpty()) { + val currentDirectory = directories.removeFirst() + + // List all of the files in the current directory + val files = currentDirectory?.listFiles() + if (files != null) { + for (file in files) { + if (file.isDirectory) { + // Add subdirectories to the list to search through + directories.add(file) + } else if (file.type?.startsWith("image/") == true) { + // Add Uri of the image file to the list + imageFileUris += file.uri + mContext.contentResolver.openInputStream(file.uri).use { input -> + if(input != null) { + val bitmap = BitmapFactory.decodeStream(input) + file.parentFile?.name?.let { + listOfProductData += ProductData( it, BBox(), bitmap) + } + input.close() + } + } + } + } + } + } + return listOfProductData + } + + /** + * This function is used to write and save the file + */ + @RequiresApi(Build.VERSION_CODES.Q) + fun saveFile(srcUri: Uri, destUri: Uri?) { + if (destUri != null) { + mContext.contentResolver.openInputStream(srcUri).use { input -> + if(input != null) { + mContext.contentResolver.openOutputStream(destUri).use { output -> + input.copyTo(output!!, DEFAULT_BUFFER_SIZE) + } + } + } + } + } + + fun saveOcrBarcodeCaptureSessionDataToPrefs(context: Context, sessionID: String, uiState: AIDataCaptureDemoUiState) { + Log.d(TAG, "saveOcrBarcodeCaptureSessionDataToPrefs: $sessionID") + val sessionData = OcrBarcodeCaptureSessionData( + ocrResults = uiState.ocrResults, + barcodeResults = uiState.barcodeResults, + captureTime = getTimeStamp(), + captureImage = uiState.captureBitmap?.let { bitmap -> + Log.d(TAG, "captureBitmap width: ${bitmap.width}") + Log.d(TAG, "captureBitmap height: ${bitmap.height}") + val outputStream = java.io.ByteArrayOutputStream() + Log.d(TAG, "outputStream size: ${outputStream.size()}") + bitmap.compress(Bitmap.CompressFormat.JPEG, 10, outputStream) + android.util.Base64.encodeToString(outputStream.toByteArray(), android.util.Base64.DEFAULT) + } ?: "" + ) + val prefs = context.getSharedPreferences("OcrBarcodeCaptureSessions", Context.MODE_PRIVATE) + val editor = prefs.edit() + val gson = Gson() + val json = gson.toJson(sessionData) + editor.putString(sessionID, json) + editor.apply() + } + + fun loadOcrBarcodeCaptureSessionDataFromPrefs(context: Context, sessionID: String): OcrBarcodeCaptureSessionData? { + val prefs = context.getSharedPreferences("OcrBarcodeCaptureSessions", Context.MODE_PRIVATE) + val json = prefs.getString(sessionID, null) + return if (json != null) { + try { + Gson().fromJson(json, OcrBarcodeCaptureSessionData::class.java) + } catch (e: Exception) { + null + } + } else { + null + } + } + + fun clearOcrBarcodeCaptureSessionPrefs(context: Context) { + val prefs = context.getSharedPreferences("OcrBarcodeCaptureSessions", Context.MODE_PRIVATE) + prefs.edit().clear().apply() + } + } +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/FilterUtils.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/FilterUtils.kt new file mode 100644 index 0000000..85598a1 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/FilterUtils.kt @@ -0,0 +1,403 @@ +package com.zebra.aidatacapturedemo.model + +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.AdvancedFilterOption +import com.zebra.aidatacapturedemo.data.CharacterMatchFilterOption +import com.zebra.aidatacapturedemo.data.CharacterTypeFilterOption +import com.zebra.aidatacapturedemo.data.OcrRegularFilterOption +import com.zebra.aidatacapturedemo.data.ResultData +import com.zebra.aidatacapturedemo.ui.view.RegexConstant +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException + +/** + * FilterUtils is a utility class that provides functions to filter OCR and Barcode result data + * based on user-selected criteria in the UI state. + */ +class FilterUtils { + companion object { + fun getOcrFilteredResultData( + uiState: AIDataCaptureDemoUiState, + outputOCRResultData: MutableList + ): List { + + val filteredOCRResultData = mutableListOf() + + when (uiState.ocrFilterData.selectedRegularFilterOption) { + OcrRegularFilterOption.UNFILTERED -> { + val regex = "(.*?)".toRegex() + for (d in outputOCRResultData) { + if (regex.matches(d.text)) { + filteredOCRResultData += ResultData( + boundingBox = d.boundingBox, + text = d.text + ) + } + } + } + + OcrRegularFilterOption.REGEX -> { + // This list holds all the possible regex patterns which includes default + additional regex strings + val regexPatternList: MutableList = mutableListOf() + + // The following validation block of code is useful for regex validation. + val regexStringDefault = + uiState.ocrFilterData.selectedRegexFilterData.regexDefaultString + if (regexStringDefault.isNotBlank()) { + + // add default regex string + validateRegexSyntax(regexStringDefault)?.let { pattern -> + regexPatternList.add(pattern) + } + + // add additional regex string(s) + uiState.ocrFilterData.selectedRegexFilterData.regexAdditionalStringList.forEach { additionalRegexString -> + if (additionalRegexString.isNotBlank()) { + validateRegexSyntax(additionalRegexString)?.let { pattern -> + regexPatternList.add(pattern) + } + } + } + } + + if (regexPatternList.isNotEmpty()) { + outputOCRResultData.forEach { resultData -> + var isRegexMatchedFound = false + + run outerLoop@{ + regexPatternList.forEach { regexPattern -> + if (regexPattern.matcher(resultData.text).matches()) { + isRegexMatchedFound = true + return@outerLoop // skip the other additional regex + } + } + } + + if (isRegexMatchedFound) { + filteredOCRResultData += ResultData( + boundingBox = resultData.boundingBox, + text = resultData.text + ) + } + } + } + } + + OcrRegularFilterOption.ADVANCED -> { + outputOCRResultData.forEach { resultData -> + var isAdvancedMatchedFound = true + + run outerLoop@{ + uiState.ocrFilterData.selectedAdvancedFilterOptionList.forEach continueNextIteration@{ advancedFilterType -> + when (advancedFilterType) { + AdvancedFilterOption.CHARACTER_TYPE -> { + val ocrCharacterTypeFilterOptionListSize = + uiState.ocrFilterData.selectedCharacterTypeFilterOptionList.size + if (ocrCharacterTypeFilterOptionListSize > 0) { + + // If Select All is selected, skip any further check, because it is considered as wildcard + if (uiState.ocrFilterData.selectedCharacterTypeFilterOptionList.contains( + CharacterTypeFilterOption.SELECT_ALL + ) + ) { + // skip other checks + } else { + + if (ocrCharacterTypeFilterOptionListSize == 1) { + if (uiState.ocrFilterData.selectedCharacterTypeFilterOptionList.contains( + CharacterTypeFilterOption.ALPHA + ) + ) { + if (RegexConstant.ALPHA_ONLY.matches( + resultData.text + ) + ) { + return@continueNextIteration // continue next iteration, CHARACTER_TYPE condition success + } else { + isAdvancedMatchedFound = false + return@outerLoop // break the loop, CHARACTER_TYPE condition failed and skip this result + } + } + + if (uiState.ocrFilterData.selectedCharacterTypeFilterOptionList.contains( + CharacterTypeFilterOption.NUMERIC + ) + ) { + if (RegexConstant.NUMERIC_ONLY.matches( + resultData.text + ) + ) { + return@continueNextIteration + } else { + isAdvancedMatchedFound = false + return@outerLoop + } + } + + if (uiState.ocrFilterData.selectedCharacterTypeFilterOptionList.contains( + CharacterTypeFilterOption.INCLUDE_SPECIAL_CHARACTERS + ) + ) { + if (RegexConstant.SPECIAL_CHARACTERS_ONLY.matches( + resultData.text + ) + ) { + return@continueNextIteration + } else { + isAdvancedMatchedFound = false + return@outerLoop + } + } + } else { // ocrCharacterTypeFilterOptionListSize == 2 + // hybrid selection found + + if (uiState.ocrFilterData.selectedCharacterTypeFilterOptionList.contains( + CharacterTypeFilterOption.ALPHA + ) && + uiState.ocrFilterData.selectedCharacterTypeFilterOptionList.contains( + CharacterTypeFilterOption.NUMERIC + ) + ) { + if (RegexConstant.ALPHA_AND_NUMERIC_ONLY.matches( + resultData.text + ) + ) { + return@continueNextIteration + } else { + isAdvancedMatchedFound = false + return@outerLoop + } + } else if (uiState.ocrFilterData.selectedCharacterTypeFilterOptionList.contains( + CharacterTypeFilterOption.ALPHA + ) && + uiState.ocrFilterData.selectedCharacterTypeFilterOptionList.contains( + CharacterTypeFilterOption.INCLUDE_SPECIAL_CHARACTERS + ) + ) { + if (RegexConstant.ALPHA_AND_SPECIAL_CHARACTERS_ONLY.matches( + resultData.text + ) + ) { + return@continueNextIteration + } else { + isAdvancedMatchedFound = false + return@outerLoop + } + } else { + if (RegexConstant.NUMERIC_AND_SPECIAL_CHARACTERS_ONLY.matches( + resultData.text + ) + ) { + return@continueNextIteration + } else { + isAdvancedMatchedFound = false + return@outerLoop + } + } + } + } + } + } + + // perform more validation if character match is selected + AdvancedFilterOption.CHARACTER_MATCH -> { + when (uiState.ocrFilterData.selectedCharacterMatchFilterData.type) { + CharacterMatchFilterOption.STARTS_WITH -> { + val areAllStringsNotBlank = + uiState.ocrFilterData.selectedCharacterMatchFilterData.startsWithStringList.all { it.isNotBlank() } + if (areAllStringsNotBlank) { + uiState.ocrFilterData.selectedCharacterMatchFilterData.startsWithStringList.forEach { startsWithString -> + if (resultData.text.startsWith( + startsWithString, + ignoreCase = true + ) + ) { + return@continueNextIteration + } + } + } + isAdvancedMatchedFound = false + return@outerLoop + } + + CharacterMatchFilterOption.CONTAINS -> { + val areAllStringsNotBlank = + uiState.ocrFilterData.selectedCharacterMatchFilterData.containsStringList.all { it.isNotBlank() } + if (areAllStringsNotBlank) { + uiState.ocrFilterData.selectedCharacterMatchFilterData.containsStringList.forEach { containString -> + if (resultData.text.contains( + containString, + ignoreCase = true + ) + ) { + return@continueNextIteration + } + } + } + isAdvancedMatchedFound = false + return@outerLoop + } + + CharacterMatchFilterOption.EXACT_MATCH -> { + val areAllStringsNotBlank = + uiState.ocrFilterData.selectedCharacterMatchFilterData.exactMatchStringList.all { it.isNotBlank() } + if (areAllStringsNotBlank) { + + uiState.ocrFilterData.selectedCharacterMatchFilterData.exactMatchStringList.forEach { exactMatchString -> + if (resultData.text.equals( + exactMatchString, + ignoreCase = true + ) + ) { + return@continueNextIteration + } + } + } + isAdvancedMatchedFound = false + return@outerLoop + } + } + } + + AdvancedFilterOption.STRING_LENGTH -> { + + if (resultData.text.length in uiState.ocrFilterData.selectedStringLengthRange.start.toInt()..uiState.ocrFilterData.selectedStringLengthRange.endInclusive.toInt()) { + return@continueNextIteration + } else { + isAdvancedMatchedFound = false + return@outerLoop + } + } + } + } + } + + if (isAdvancedMatchedFound) { + filteredOCRResultData += ResultData( + boundingBox = resultData.boundingBox, + text = resultData.text + ) + } + } + } + + } + + return filteredOCRResultData + } + + fun getBarcodeFilteredResultData( + uiState: AIDataCaptureDemoUiState, + outputBarcodeResultData: MutableList + ): List { + + val filteredBarcodeResultData = mutableListOf() + + outputBarcodeResultData.forEach { resultData -> + var isBarcodeMatchedFound = true + + run outerLoop@{ + uiState.barcodeFilterData.selectedAdvancedFilterOptionList.forEach continueNextIteration@{ advancedFilterType -> + + when (advancedFilterType) { + AdvancedFilterOption.CHARACTER_MATCH -> { + when (uiState.barcodeFilterData.selectedCharacterMatchFilterData.type) { + CharacterMatchFilterOption.STARTS_WITH -> { + val areAllStringsNotBlank = + uiState.barcodeFilterData.selectedCharacterMatchFilterData.startsWithStringList.all { it.isNotBlank() } + if (areAllStringsNotBlank) { + uiState.barcodeFilterData.selectedCharacterMatchFilterData.startsWithStringList.forEach { startsWithString -> + if (resultData.text.startsWith( + startsWithString, + ignoreCase = true + ) + ) { + return@continueNextIteration + } + } + } + isBarcodeMatchedFound = false + return@outerLoop + } + + CharacterMatchFilterOption.CONTAINS -> { + val areAllStringsNotBlank = + uiState.barcodeFilterData.selectedCharacterMatchFilterData.containsStringList.all { it.isNotBlank() } + if (areAllStringsNotBlank) { + uiState.barcodeFilterData.selectedCharacterMatchFilterData.containsStringList.forEach { containString -> + if (resultData.text.contains( + containString, + ignoreCase = true + ) + ) { + return@continueNextIteration + } + } + } + isBarcodeMatchedFound = false + return@outerLoop + } + + CharacterMatchFilterOption.EXACT_MATCH -> { + val areAllStringsNotBlank = + uiState.barcodeFilterData.selectedCharacterMatchFilterData.exactMatchStringList.all { it.isNotBlank() } + if (areAllStringsNotBlank) { + uiState.barcodeFilterData.selectedCharacterMatchFilterData.exactMatchStringList.forEach { exactMatchString -> + if (resultData.text.equals( + exactMatchString, + ignoreCase = true + ) + ) { + return@continueNextIteration + } + } + } + isBarcodeMatchedFound = false + return@outerLoop + } + } + } + + AdvancedFilterOption.STRING_LENGTH -> { + if (resultData.text.length in uiState.barcodeFilterData.selectedStringLengthRange.start.toInt()..uiState.barcodeFilterData.selectedStringLengthRange.endInclusive.toInt()) { + return@continueNextIteration + } else { + isBarcodeMatchedFound = false + return@outerLoop + } + } + + else -> { + TODO("Unhandled barcode filter received = $advancedFilterType") + } + } + } + } + + if (isBarcodeMatchedFound) { + filteredBarcodeResultData += ResultData( + boundingBox = resultData.boundingBox, + text = resultData.text + ) + } + } + + return filteredBarcodeResultData + } + + fun validateRegexSyntax(regexString: String): Pattern? { + + // replace if any '\\' found on the regex with '\' as sometime user may get this online suggestion + val userInputString = regexString.replace( + "\\\\", + "\\" + ) + + return try { + Pattern.compile(userInputString) + } catch (e: PatternSyntaxException) { + null + } + } + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/GenericEntityTrackerAnalyzer.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/GenericEntityTrackerAnalyzer.kt new file mode 100644 index 0000000..ca37634 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/GenericEntityTrackerAnalyzer.kt @@ -0,0 +1,253 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.model + +import android.graphics.Rect +import android.util.Log +import androidx.camera.core.ImageAnalysis +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.coroutineScope +import com.zebra.ai.vision.analyzer.tracking.EntityTrackerAnalyzer +import com.zebra.ai.vision.detector.BarcodeDecoder +import com.zebra.ai.vision.detector.Detector +import com.zebra.ai.vision.detector.ModuleRecognizer +import com.zebra.ai.vision.detector.TextOCR +import com.zebra.ai.vision.entity.BarcodeEntity +import com.zebra.ai.vision.entity.Entity +import com.zebra.ai.vision.entity.LabelEntity +import com.zebra.ai.vision.entity.ParagraphEntity +import com.zebra.ai.vision.entity.ProductEntity +import com.zebra.ai.vision.entity.ShelfEntity +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.AdvancedFilterOption +import com.zebra.aidatacapturedemo.data.DetectionLevel +import com.zebra.aidatacapturedemo.data.ModuleData +import com.zebra.aidatacapturedemo.data.OcrRegularFilterOption +import com.zebra.aidatacapturedemo.data.ResultData +import com.zebra.aidatacapturedemo.data.UsecaseState +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + + +/** + * [GenericEntityTrackerAnalyzer] class is used to detect & Track barcodes, ocr and shelf data + * found on the Camera Live Preview + * + * @param uiState - Used to read all the UI Current State + * @param viewModel - Used to write any UI State Changes via [AIDataCaptureDemoViewModel] + */ +class GenericEntityTrackerAnalyzer(val uiState: StateFlow, val viewModel: AIDataCaptureDemoViewModel) { + + private lateinit var mActivityLifecycle: Lifecycle + private val TAG = "GenericEntityTrackerAnalyzer" + private val executorService: ExecutorService = Executors.newSingleThreadExecutor() + private var detectors: MutableList>> = mutableListOf() + + fun addDecoder(detector : Detector>){ + detectors.add(detector) + } + + fun setupEntityTrackerAnalyzer(myLifecycle: Lifecycle): EntityTrackerAnalyzer { + mActivityLifecycle = myLifecycle + + val entityTrackerAnalyzer = when(uiState.value.usecaseSelected){ + UsecaseState.OCRBarcodeFind.value -> { + EntityTrackerAnalyzer( + detectors, + ImageAnalysis.COORDINATE_SYSTEM_ORIGINAL, + executorService, + ::handleEntitiesOcrBarcodeFilter + ) + } + else -> { + EntityTrackerAnalyzer( + detectors, + ImageAnalysis.COORDINATE_SYSTEM_ORIGINAL, + executorService, + ::handleEntities + ) + } + } + + return entityTrackerAnalyzer + } + + private fun handleEntities(result: EntityTrackerAnalyzer.Result) { + mActivityLifecycle.coroutineScope.launch(Dispatchers.Main) { + detectors.forEach { detector -> + if (detector is BarcodeDecoder) { + val returnEntityList = result.getValue(detector) + var rectList: MutableList = mutableListOf() + returnEntityList?.forEach { entity -> + if (entity != null) { + val barcodeEntity = entity as BarcodeEntity + val value = barcodeEntity.value + val rect = barcodeEntity.boundingBox + rectList += ResultData(boundingBox = rect, text = value) + } + } + viewModel.updateBarcodeResultData(results = rectList) + } else if ( detector is TextOCR) { + val returnEntityList = result.getValue(detector) + val outputOCRResultData = mutableListOf() + returnEntityList?.forEach { entity -> + if (entity != null) { + val paragraphEntity = entity as ParagraphEntity + val lines = paragraphEntity.lines + for (line in lines) { + for (word in line.words) { + val bbox = word.complexBBox + + if (bbox != null && bbox.x != null && bbox.y != null && bbox.x.size == 4 && bbox.y.size == 4) { + val minX = bbox.x[0] + val maxX = bbox.x[2] + val minY = bbox.y[0] + val maxY = bbox.y[2] + + val rect = Rect(minX.toInt(), minY.toInt(), maxX.toInt(), maxY.toInt()) + val decodedValue = word.text + outputOCRResultData.add( + ResultData( + boundingBox = rect, + text = decodedValue + ) + ) + } + } + } + } + } + viewModel.updateOcrResultData(results = outputOCRResultData) + } else if (detector is ModuleRecognizer) { + val returnEntityList = result.getValue(detector) + val shelves = mutableListOf() + val labels = mutableListOf() + val products = mutableListOf() + returnEntityList?.forEach { entity -> + when (entity) { + is ShelfEntity -> shelves.add(entity) + is LabelEntity -> labels.add(entity) + is ProductEntity -> products.add(entity) + } + } + viewModel.updateModuleRecognitionResult(ModuleData(shelves, labels, products)) + } + else { + Log.e(TAG, "handleEntities => Unknown detector type found = $detector ") + } + } + } + } + + private fun handleEntitiesOcrBarcodeFilter(result: EntityTrackerAnalyzer.Result) { + mActivityLifecycle.coroutineScope.launch(Dispatchers.Main) { + detectors.forEach { detector -> + if (detector is BarcodeDecoder) { + val returnEntityList = result.getValue(detector) + var rectList: MutableList = mutableListOf() + returnEntityList?.forEach { entity -> + if (entity != null) { + val barcodeEntity = entity as BarcodeEntity + val value = barcodeEntity.value + val rect = barcodeEntity.boundingBox + rectList += ResultData(boundingBox = rect, text = value) + } + } + + // If feedbackSettings.showDetectedBarcode is false -> then don't show the undecoded barcodes on the display + if (!uiState.value.ocrBarcodeFindSettings.feedbackSettings.showDetectedBarcode){ + rectList.retainAll { it.text.isNotBlank() } + } + + viewModel.updateBarcodeResultData( + results = FilterUtils.getBarcodeFilteredResultData( + uiState = uiState.value, + outputBarcodeResultData = rectList + ) + ) + + + } else if ( detector is TextOCR) { + val returnEntityList = result.getValue(detector) + val outputOCRResultData = mutableListOf() + + if ((uiState.value.ocrFilterData.selectedRegularFilterOption == OcrRegularFilterOption.REGEX && uiState.value.ocrFilterData.selectedRegexFilterData.detectionLevel == DetectionLevel.LINE) || + (uiState.value.ocrFilterData.selectedRegularFilterOption == OcrRegularFilterOption.ADVANCED) && + uiState.value.ocrFilterData.selectedAdvancedFilterOptionList.contains( + AdvancedFilterOption.CHARACTER_MATCH) && + uiState.value.ocrFilterData.selectedCharacterMatchFilterData.detectionLevel == DetectionLevel.LINE){ + + // Level.LINE_LEVEL results must be displayed + returnEntityList?.forEach { entity -> + if (entity != null) { + val paragraphEntity = entity as ParagraphEntity + val lines = paragraphEntity.lines + for (line in lines) { + val bbox = line.complexBBox + + if (bbox != null && bbox.x != null && bbox.y != null && bbox.x.size == 4 && bbox.y.size == 4) { + val minX = bbox.x[0] + val maxX = bbox.x[2] + val minY = bbox.y[0] + val maxY = bbox.y[2] + + val rect = Rect(minX.toInt(), minY.toInt(), maxX.toInt(), maxY.toInt()) + val decodedValue = line.text + outputOCRResultData.add( + ResultData( + boundingBox = rect, + text = decodedValue + ) + ) + } + } + } + } + }else{ + // Level.WORD_LEVEL results must be displayed + returnEntityList?.forEach { entity -> + if (entity != null) { + val paragraphEntity = entity as ParagraphEntity + val lines = paragraphEntity.lines + for (line in lines) { + for (word in line.words) { + val bbox = word.complexBBox + + if (bbox != null && bbox.x != null && bbox.y != null && bbox.x.size == 4 && bbox.y.size == 4) { + val minX = bbox.x[0] + val maxX = bbox.x[2] + val minY = bbox.y[0] + val maxY = bbox.y[2] + + val rect = Rect(minX.toInt(), minY.toInt(), maxX.toInt(), maxY.toInt()) + val decodedValue = word.text + outputOCRResultData.add( + ResultData( + boundingBox = rect, + text = decodedValue + ) + ) + } + } + } + } + } + } + + viewModel.updateOcrResultData( + results = FilterUtils.getOcrFilteredResultData( + uiState = uiState.value, + outputOCRResultData = outputOCRResultData + ) + ) + } else { + Log.e(TAG, "handleEntities => Unknown detector type found = $detector ") + } + } + } + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/ProductEnrollmentRecognition.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/ProductEnrollmentRecognition.kt new file mode 100644 index 0000000..cdff0f7 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/ProductEnrollmentRecognition.kt @@ -0,0 +1,513 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.model + +import android.graphics.Bitmap +import android.graphics.Matrix +import android.util.Log +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import com.zebra.ai.vision.detector.AIVisionSDKException +import com.zebra.ai.vision.detector.BBox +import com.zebra.ai.vision.detector.FeatureExtractor +import com.zebra.ai.vision.detector.FeatureStorage +import com.zebra.ai.vision.detector.InvalidInputException +import com.zebra.ai.vision.detector.Localizer +import com.zebra.ai.vision.detector.Recognizer +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.ProductData +import com.zebra.aidatacapturedemo.data.toProductData +import com.zebra.aidatacapturedemo.model.FileUtils.Companion.databaseFile +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import java.io.IOException +import java.nio.file.Paths +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import kotlin.time.TimeSource + +/** + * [ProductEnrollmentRecognition] class is used to perform the product recognition on the Camera Live Preview. + * It uses the Localizer to detect shelves, labels, peg labels, products which generates + * boundingboxes for the detections. + * FeatureExtractor is used to extract features from product detection bounding boxes. + * Recognizer does a semantic searches to locate matching descriptors, and + * finds the best fit from feature vectors. + * FeatureStorage is used save feature descriptors, which is used in conjunction with feature extractor, + * to enroll new products for product recognition. + * It provides the methods to initialize, deinitialize, execute, + * deleteProductDB, applyProductDB and enrollProductIndex. + * @param uiState - Used to read all the UI Current State + * @param viewModel - Used to write any UI State Changes via [AIDataCaptureDemoViewModel] + * @param cacheDir - App Cache Directory Path required for loading the Product.db to enroll & Recognize Retail Products + */ + +class ProductEnrollmentRecognition( + val uiState: StateFlow, + val viewModel: AIDataCaptureDemoViewModel, + private val cacheDir: String +) : ImageAnalysis.Analyzer { + + private var mIsStopPreviewAnalysisRequested: Boolean = false + private val TAG = "ProductEnrollmentRecognition" + + private var localizer: Localizer? = null + private var extractor: FeatureExtractor? = null + private var featureStorage: FeatureStorage? = null + private var recognizer: Recognizer? = null + private val job = Job() + private val executorService: ExecutorService = Executors.newFixedThreadPool(4) + private val scope = CoroutineScope(Dispatchers.IO + job) + private var isAnalyzing = true + + /** + * ProductEnrollmentRecognition workflow takes the input image and runs it through the + * localizer that generates boundingboxes for the shelf, label, and product detections. + * We use the product detection boundingboxes, which is passed along with the input image + * to extract features using the feature extractor. Feature extractor generates feature + * descriptos which is used to perform semantic search using the recognizer to find + * the best fitting product from the database. + * We use only products with greater than 0.8 confidence from product recognition. + */ + override fun analyze(image: ImageProxy) { + if (!uiState.value.isRetailShelfModelDemoReady) { + Log.e(TAG, "ProductEnrollmentRecognition init in progress") + image.close() + return + } + if (!isAnalyzing || mIsStopPreviewAnalysisRequested) { + image.close() + return + } + + isAnalyzing = false // Set to false to prevent re-entry + + scope.launch { + try { + Log.d(TAG, "Starting image analysis") + val bitmap = viewModel.rotateBitmapIfNeeded(image)!! + execute(bitmap) + image.close() + } catch (e: InvalidInputException) { + Log.e(TAG, e.message ?: "InvalidInputException occurred") + image.close() + } catch (e: AIVisionSDKException) { + Log.e(TAG, e.message ?: "AIVisionSDKException occurred") + image.close() + } finally { + isAnalyzing = true + } + } + } + + fun startAnalyzing() { + isAnalyzing = true + } + + fun stopAnalyzing() { + isAnalyzing = false + } + + /** + * This function is used to initialize the various components that are used to + * accomplish product recognition, namely localizer, feature extractor and + * product recognition. feature storage in is used to save new products decriptors. + */ + fun initialize() { + deinitialize() + updateRetailShelfModelDemoReady(false) + initializeLocalizer() + initFeatureStorage() + initFeatureExtractor() + initProductRecognition() + } + + /** + * To deinitialize the ProductEnrollmentRecognition, we need to dispose the localizer, + * feature extractor, feature storage and recognizer. + */ + fun deinitialize() { + localizer?.dispose() + localizer = null + recognizer?.dispose() + recognizer = null + featureStorage?.dispose() + featureStorage = null + extractor?.dispose() + extractor = null + } + + /** + * ProductRecognition workflow takes the input image and runs it through the + * localizer that generates boundingboxes for the shelf, label, and product detections. + * WE use the product detection boundingboxes, which is passed along with the input image + * to extract features using the feature extractor. Feature extractor generates feature + * descriptos which is used to perform semantic search using the recognizer to find + * the best fitting product from the database. + * We use only products with greater than 0.8 confidence from product recognition. + */ + fun execute(bitmap: Bitmap, isCapturedUseCase: Boolean = false) { + if (bitmap != null) { + val bboxes = executeRetailShelfLocalization(bitmap) + if (bboxes != null) { + executeProductRecognition( + bitmap = bitmap, + bboxes = bboxes, + isCapturedUseCase = isCapturedUseCase + ) + } + } + } + + /** + * To initialize the RetailShelfLocalizer, we need to set the + * model name, processor type (CPU, GPU, DSP), and + * dimensions of the input image + */ + private fun initializeLocalizer() { + Log.i(TAG, "initializeLocalizer") + localizer?.dispose() + localizer = null + + val locSettings = Localizer.Settings("product-and-shelf-recognizer") + + //Swap the values as the presented index is reverse of what model expects + val processorOrder = when (uiState.value.productRecognitionSettings.commonSettings.processorSelectedIndex) { + 0 -> arrayOf(2, 0, 1) // AUTO + 1 -> arrayOf(2) // DSP + 2 -> arrayOf(1) // GPU + 3 -> arrayOf(0) //CPU + else -> { + arrayOf(2, 0, 1) + } + } + + locSettings.inferencerOptions.runtimeProcessorOrder = processorOrder + + locSettings.inferencerOptions.defaultDims.width = uiState.value.productRecognitionSettings.commonSettings.inputSizeSelected + locSettings.inferencerOptions.defaultDims.height = uiState.value.productRecognitionSettings.commonSettings.inputSizeSelected + + try { + Localizer.getLocalizer(locSettings, executorService) + .thenAccept { localizerInstance: Localizer -> + localizer = localizerInstance + updateRetailShelfModelDemoReady(true) + Log.i(TAG, "Localizer init Success") + }.exceptionally { e: Throwable -> + Log.e(TAG, "Localizer init Failed -> " + e.message) + if (e.message?.contains("Given runtimes are not available") == true) { + viewModel.updateToastMessage(message = "Selected inference type is not supported on this device. Switching to Auto-select for optimal performance.") + viewModel.updateSelectedProcessor(0) //Auto-Select + viewModel.saveSettings() + initializeLocalizer() + } + null + } + } catch (e: IOException) { + Log.e(TAG, "Localizer init Failed -> " + e.message) + } + } + + /** + * To initialize the FeatureExtractor, we need to set the + * model name, processor type (CPU, GPU, DSP) + */ + private fun initFeatureExtractor() { + Log.i(TAG, "initFeatureExtractor") + extractor?.dispose() + extractor = null + + val extractorSettings = + FeatureExtractor.Settings("product-and-shelf-recognizer") + + //Swap the values as the presented index is reverse of what model expects + val processorOrder = when (uiState.value.productRecognitionSettings.commonSettings.processorSelectedIndex) { + 0 -> arrayOf(2, 0, 1) // AUTO + 1 -> arrayOf(2) // DSP + 2 -> arrayOf(1) // GPU + 3 -> arrayOf(0) //CPU + else -> { + arrayOf(2, 0, 1) + } + } + extractorSettings.inferencerOptions.runtimeProcessorOrder = processorOrder + + try { + FeatureExtractor.getFeatureExtractor(extractorSettings, executorService) + .thenAccept { extractorInstance: FeatureExtractor -> + extractor = extractorInstance + updateRetailShelfModelDemoReady(true) + Log.i(TAG, "Feature Extractor init Success") + }.exceptionally { e: Throwable -> + Log.e(TAG, "Feature Extractor init Failed -> " + e.message) + if (e.message?.contains("Given runtimes are not available") == true) { + viewModel.updateSelectedProcessor(0) //Auto-Select + viewModel.saveSettings() + initFeatureExtractor() + } + null + } + } catch (e: IOException) { + Log.e(TAG, "Feature Extractor Failed -> " + e.message) + } + } + + /** + * To initialize the FeatureStorage, we need to set the + * database file path where the features are stored and max update N. + * We could potentially use this database file initialize a FeatureExtractor + * to enroll more products for recognition + */ + private fun initFeatureStorage() { + Log.i(TAG, "initFeatureStorage") + + featureStorage?.dispose() + featureStorage = null + + val dataBaseFile = Paths.get(cacheDir, databaseFile).toString() + val featureStorageSettings = FeatureStorage.Settings(dataBaseFile) + featureStorageSettings.maxUpdateN = 5 + + try { + FeatureStorage.getFeatureStorage(featureStorageSettings, executorService) + .thenAccept { storageInstance: FeatureStorage -> + featureStorage = storageInstance + updateRetailShelfModelDemoReady(true) + Log.i(TAG, "Feature Storage init Success") + }.exceptionally { e: Throwable -> + Log.e(TAG, "Feature Storage init Failed -> " + e.message) + null + } + } catch (e: IOException) { + Log.e(TAG, "Feature Storage init Failed -> " + e.message) + } catch (e: RuntimeException) { + Log.e(TAG, "DB empty -> " + e.message) + } + } + + // + /** To initialize the Recognizer, we need to set the database file path + * and index dimensions + */ + private fun initProductRecognition(isEnrollmentRequested: Boolean = false) { + Log.i(TAG, "initProductEnrollmentRecognition") + recognizer?.dispose() + recognizer = null + + val recognizerSettings = Recognizer.SettingsDb() + val indexDimensions = 768 + recognizerSettings.dbSource = Paths.get(cacheDir, databaseFile).toString() + recognizerSettings.indexDimensions = indexDimensions + + try { + Recognizer.getRecognizer(recognizerSettings, executorService) + .thenAccept { recognizerInstance: Recognizer -> + recognizer = recognizerInstance + if (isEnrollmentRequested) { + updateProductEnrollmentState(state = true) + } else { + updateRetailShelfModelDemoReady(true) + } + Log.i(TAG, "Recognizer init Success") + }.exceptionally { e: Throwable -> + Log.e(TAG, "Recognizer init Failed -> " + e.message) + null + } + } catch (e: IOException) { + Log.e(TAG, "Recognizer init Failed -> " + e.message) + } catch (e: RuntimeException) { + updateRetailShelfModelDemoReady(true) + Log.e(TAG, "DB empty -> " + e.message) + } + } + + /** + * This function is used to delete the product database file. We need to reinitialize + * FeatureStorage and Recognizer with new empty database file. + * This will result in no products being recognized. + */ + fun deleteProductDB() { + Log.i(TAG, "deleteProductDB") + FileUtils.deleteProductDBFile() + initFeatureStorage() + initProductRecognition() + } + + /** + * This function is used to apply the product database file. + * We need to reinitialize FeatureStorage and Recognizer with the new + * database file. This will result in the products included in the database + * being recognized. + */ + fun applyProductDB() { + Log.i(TAG, "applyProductDB") + initFeatureStorage() + initProductRecognition() + } + + /** + * This function is used to enroll the products, using their product decriptors + * into the database. We need to extract the features from the product detection + * bounding boxes and add them to the feature storage. + * We then reinitialize the product recognition to include the new products. + */ + fun enrollProductIndex(productDataList: List) { + Log.i(TAG, "enrollProducts") + if (productDataList.size == 0) { + return + } + Log.i(TAG, "Num Products - ${productDataList.size}") + scope.launch { + for (product in productDataList) { + if (product.text.isNotEmpty()) { + val arrayOfDescriptor = + extractor?.generateSingleDescriptor(product.crop, executorService)?.get() + featureStorage!!.addDescriptors(product.text, arrayOfDescriptor, true) + } + } + initProductRecognition(isEnrollmentRequested = true) + } + } + + /** + * This function is used to execute the retail shelf localization. + * Localizer generates boundingboxes for the shelf, shelf labels, peg labels, + * and product detections. + */ + private fun executeRetailShelfLocalization(bitmap: Bitmap?): Array? { + Log.i(TAG, "executeRetailShelfLocalization") + Log.i(TAG, "Image Width = " + bitmap?.width.toString()) + Log.i(TAG, "Image Height = " + bitmap?.height.toString()) + val timeSource = TimeSource.Monotonic + val mark = timeSource.markNow() + val result = localizer?.detect(bitmap, executorService)?.get() + val elapsed = timeSource.markNow() - mark + updateRetailShelfDetectionResult(result) + return result + } + + /** + * This function is used to execute the product recognition. + * We use the product detection boundingboxes, which is passed along with the + * input image to extract features using the feature extractor. + * Feature extractor generates feature descriptors which is used to perform + * semantic search using the recognizer to find the best fitting product + * from the database. + * We use only products with greater than 0.8 confidence from product recognition. + */ + private fun executeProductRecognition( + bitmap: Bitmap?, + bboxes: Array, + isCapturedUseCase: Boolean + ) { + Log.i(TAG, "executeProductRecognition") + if (recognizer == null) { + + // When no Product Database is found, the recognizer won't execute, resulted in empty productResultsList. + // hence let's create a productResultsList assuming product SKU as empty + if (isCapturedUseCase) { + updateProductResults(prepareProductDataList(bBoxes = bboxes, bitmap = bitmap!!)) + } + return + } // empty db + + val timeSource = TimeSource.Monotonic + val mark = timeSource.markNow() + + val products: Array = bboxes.filter { it.cls == 1 }.toTypedArray() + Log.i(TAG, "Products - ${products.size}") + + if (products.isNotEmpty()) { + try { + val descriptors = + extractor?.generateDescriptors(products, bitmap, executorService)?.get() + + val elapsed = timeSource.markNow() - mark + Log.d(TAG, "Extractor - ${elapsed}") + descriptors?.let { + val mark2 = timeSource.markNow() + val recognitions = + recognizer?.findRecognitions(descriptors, executorService)?.get() + Log.i(TAG, "Recognitions - ${recognitions?.size}") + + recognitions?.let { it1 -> + toProductData(viewModel.uiState.value.productRecognitionSettings.similarityThreshold/100f, + bitmap!!, products, + it1 + ) + }?.let { it2 -> + updateProductResults(it2) + } + val elapsed2 = timeSource.markNow() - mark2 + Log.d(TAG, "Recognizer - ${elapsed2}") + } + } catch (e: InvalidInputException) { + Log.e(TAG, "Exception = ${e.message}") + } + } else { + updateProductResults(null) + } + } + + private fun prepareProductDataList(bBoxes : Array, bitmap: Bitmap): MutableList { + val productDataList = mutableListOf() + bBoxes.filter { it.cls == 1 }.forEach { productBbox -> + productDataList.add( + ProductData( + bBox = productBbox, + text = "", + crop = + Bitmap.createBitmap( + bitmap, + productBbox.xmin.toInt(), + productBbox.ymin.toInt(), + (productBbox.xmax - productBbox.xmin).toInt(), + (productBbox.ymax - productBbox.ymin).toInt() + ) + ) + ) + } + return productDataList + } + + private fun updateRetailShelfModelDemoReady(isReady: Boolean) { + if (isReady) { + if (localizer != null && featureStorage != null && extractor != null) { + viewModel.updateRetailShelfModelDemoReady(isReady = true) + } + } else { + viewModel.updateRetailShelfModelDemoReady(isReady = false) + + } + } + + fun updateProductResults(results: MutableList?) { + viewModel.updateProductRecognitionResult(results = results) + } + + fun updateProductEnrollmentState(state: Boolean) { + viewModel.updateProductEnrollmentState(state = state) + } + + private fun updateRetailShelfDetectionResult(result: Array?) { + viewModel.updateRetailShelfDetectionResult(results = result) + } + + fun stopPreviewAnalysis() { + mIsStopPreviewAnalysisRequested = true + } + + fun executeHighRes(highResBitmap: Bitmap) { + while (!isAnalyzing) { + } + execute(bitmap = highResBitmap, isCapturedUseCase = true) + } + + fun startPreviewAnalysis() { + mIsStopPreviewAnalysisRequested = false + } +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/RetailShelfAnalyzer.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/RetailShelfAnalyzer.kt new file mode 100644 index 0000000..05aae84 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/RetailShelfAnalyzer.kt @@ -0,0 +1,131 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.model + +import android.util.Log +import com.zebra.ai.vision.detector.BBox +import com.zebra.ai.vision.detector.BarcodeDecoder +import com.zebra.ai.vision.detector.EntityType +import com.zebra.ai.vision.detector.ModuleRecognizer +import com.zebra.ai.vision.entity.LocalizerEntity +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.model.FileUtils.Companion.databaseFile +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import kotlinx.coroutines.flow.StateFlow +import java.io.IOException +import java.nio.file.Paths +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +/** + * [RetailShelfAnalyzer] class is used to detect all the SHELF LABELS, PEG LABELS, PRODUCTS & SHELVES of the Retail Store + * Shelves found on the Camera Live Preview. + * + * @param uiState - Used to read all the UI Current State + * @param viewModel - Used to write any UI State Changes via [AIDataCaptureDemoViewModel] + */ +class RetailShelfAnalyzer( + val uiState: StateFlow, + val viewModel: AIDataCaptureDemoViewModel, + private val cacheDir: String +) { + + private val TAG = "RetailShelfAnalyzer" + private var moduleRecognizer: ModuleRecognizer? = null + private val mavenBarcodeModelName = "barcode-localizer" + private val mavenOCRModelName = "text-ocr-recognizer" + private val mavenProductModelName = "product-and-shelf-recognizer" + private val moduleRecognizerSettings = ModuleRecognizer.Settings(mavenProductModelName) + private val executorService: ExecutorService = Executors.newSingleThreadExecutor() + + /** + * initialize function is used to initialize the ModuleRecognizer for Retail Shelf Analyzer + * use case. It configures the model settings based on the current UI state and creates an + * instance of ModuleRecognizer. If the initialization fails due to unsupported inference type + * or missing product data, it updates the UI with appropriate messages and + * takes corrective actions. + */ + fun initialize() { + Log.e(TAG, "Initializing ModuleRecognizer for EntityTrackerAnalyzer") + + moduleRecognizer?.dispose() + moduleRecognizer = null + updateRetailShelfModelDemoReady(false) + + configure() + + val startTime = System.currentTimeMillis() + try { + ModuleRecognizer.getModuleRecognizer(moduleRecognizerSettings, executorService) + .thenAccept { recognizerInstance -> + Log.e(TAG, "ModuleRecognizer instance created") + moduleRecognizer = recognizerInstance + updateRetailShelfModelDemoReady(true) + Log.d(TAG, "Product Recognition creation time: ${System.currentTimeMillis() - startTime} ms") + }.exceptionally { e: Throwable -> + Log.e(TAG, "ModuleRecognizer init Failed -> " + e.message) + if (e.message?.contains("Given runtimes are not available") == true || + e.message?.contains("Initialize barcodeDecoder due to SNPE exception") == true + ) { + viewModel.updateToastMessage(message = "Selected inference type is not supported on this device. Switching to Auto-select for optimal performance.") + viewModel.updateSelectedProcessor(0) //Auto-Select + viewModel.saveSettings() + initialize() + } else if ((e.message?.contains("No DB product data available to build a search index!") == true) || + (e.message?.contains("Cannot open DB file") == true)) { + viewModel.updateToastMessage(message = "No products enrolled. Enroll products using Product & Shelf Enrollment") + } + null + } + } catch (e: IOException) { + Log.e(TAG, "ModuleRecognizer init Failed -> " + e.message) + } + } + + private fun configure() { + try { + val dataBaseFile = Paths.get(cacheDir, databaseFile).toString() + //Swap the values as the presented index is reverse of what model expects + val processorOrder = + when (uiState.value.retailShelfSettings.commonSettings.processorSelectedIndex) { + 0 -> arrayOf(2, 0, 1) // AUTO + 1 -> arrayOf(2) // DSP + 2 -> arrayOf(1) // GPU + 3 -> arrayOf(0) //CPU + else -> { + arrayOf(2, 0, 1) + } + } + moduleRecognizerSettings.inferencerOptions.runtimeProcessorOrder = processorOrder + moduleRecognizerSettings.inferencerOptions.defaultDims.width = + uiState.value.retailShelfSettings.commonSettings.inputSizeSelected + moduleRecognizerSettings.inferencerOptions.defaultDims.height = + uiState.value.retailShelfSettings.commonSettings.inputSizeSelected + + val labelBarcodeSettings: BarcodeDecoder.Settings = BarcodeDecoder.Settings(mavenBarcodeModelName) + val barcodeSettingsMap: MutableMap = HashMap() + barcodeSettingsMap[EntityType.LABEL] = labelBarcodeSettings + moduleRecognizerSettings.enableBarcodeRecognition(barcodeSettingsMap) + + moduleRecognizerSettings.enableProductRecognitionWithDb( + mavenProductModelName, + dataBaseFile + ) + } catch (e: Exception) { + Log.e(TAG, "Fatal error: configure failed - ${e.message}") + } + } + + fun deinitialize() { + moduleRecognizer?.dispose() + moduleRecognizer = null + } + + fun getDetector(): ModuleRecognizer? { + return moduleRecognizer + } + + private fun updateRetailShelfModelDemoReady(isReady: Boolean) { + viewModel.updateRetailShelfModelDemoReady(isReady = isReady) + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/TextOCRAnalyzer.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/TextOCRAnalyzer.kt new file mode 100644 index 0000000..fd1acc6 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/TextOCRAnalyzer.kt @@ -0,0 +1,304 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.model + +import android.graphics.Bitmap +import android.graphics.Rect +import android.util.Log +import com.zebra.ai.vision.detector.AIVisionSDKException +import com.zebra.ai.vision.detector.ImageData +import com.zebra.ai.vision.detector.InvalidInputException +import com.zebra.ai.vision.detector.TextOCR +import com.zebra.ai.vision.entity.ParagraphEntity +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.AdvancedFilterOption +import com.zebra.aidatacapturedemo.data.DetectionLevel +import com.zebra.aidatacapturedemo.data.OcrRegularFilterOption +import com.zebra.aidatacapturedemo.data.PROFILING +import com.zebra.aidatacapturedemo.data.ResultData +import com.zebra.aidatacapturedemo.data.UsecaseState +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import kotlinx.coroutines.flow.StateFlow +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + + +/** + * [TextOCRAnalyzer] class is used to detect all the Optical Character Recognition (OCR) found on the Camera Live Preview + * + * @param uiState - Used to read all the UI Current State + * @param viewModel - Used to write any UI State Changes via [AIDataCaptureDemoViewModel] + */ +class TextOCRAnalyzer( + val uiState: StateFlow, + val viewModel: AIDataCaptureDemoViewModel +) { + private val TAG = "TextOCRAnalyzer" + + private var textOCR: TextOCR? = null + private val textOCRSettings = TextOCR.Settings("text-ocr-recognizer") + private val executorService: ExecutorService = Executors.newSingleThreadExecutor() + + /** + * initialize function is used to initialize the TextOCR model with the specified settings + * and handle any exceptions that may occur during the initialization process. + * It also updates the UI state to indicate whether the OCR model is ready or not. + */ + fun initialize() { + try { + textOCR?.dispose() + textOCR = null + updateOcrModelDemoReady(false) + + configure() + + val mStart = System.currentTimeMillis() + TextOCR.getTextOCR(textOCRSettings, executorService).thenAccept { ocrInstance -> + textOCR = ocrInstance + updateOcrModelDemoReady(true) + Log.e( + PROFILING, + "TextOCR() obj creation / model loading time = ${System.currentTimeMillis() - mStart} milli sec" + ) + Log.i(TAG, "TextOCR creation success") + }.exceptionally { e -> + Log.e(TAG, "Fatal error: TextOCR creation failed - ${e.message}") + if ((e.message?.contains("Given runtimes are not available") == true) || + (e.message?.contains("Error creating SNPE object") == true)) { + viewModel.updateToastMessage(message = "Selected inference type is not supported on this device. Switching to Auto-select for optimal performance.") + viewModel.updateSelectedProcessor(0) //Auto-Select + viewModel.saveSettings() + initialize() + } + null + } + } catch (e: Exception) { + Log.e(TAG, "Fatal error: load failed - ${e.message}") + } + } + + fun deinitialize() { + textOCR?.dispose() + textOCR = null + } + + fun getDetector() : TextOCR? { + return textOCR + } + + /** executeHighRes function is used to perform OCR analysis on a high-resolution bitmap image. + * It submits the analysis task to an executor service, processes the image data, and updates + * the UI state with the OCR results. + * The function also handles exceptions that may occur during the analysis process. + * + * @param highResBitmap - The high-resolution bitmap image to be analyzed for OCR. + */ + fun executeHighRes(highResBitmap: Bitmap) { + executorService.submit { + try { + Log.d(TAG, "Starting image analysis") + val highResImageData: ImageData = ImageData.fromBitmap(highResBitmap, 0) + textOCR?.process(highResImageData) + ?.thenAccept { result -> + if ((uiState.value.ocrFilterData.selectedRegularFilterOption == OcrRegularFilterOption.REGEX && uiState.value.ocrFilterData.selectedRegexFilterData.detectionLevel == DetectionLevel.LINE) || + (uiState.value.ocrFilterData.selectedRegularFilterOption == OcrRegularFilterOption.ADVANCED) && + uiState.value.ocrFilterData.selectedAdvancedFilterOptionList.contains( + AdvancedFilterOption.CHARACTER_MATCH + ) && + uiState.value.ocrFilterData.selectedCharacterMatchFilterData.detectionLevel == DetectionLevel.LINE + ) { + onDetectionTextResultLineLevel(result) + } else { + onDetectionTextResultWordLevel(result) + } + } + } catch (e: InvalidInputException) { + Log.e(TAG, e.message ?: "InvalidInputException occurred") + } catch (e: AIVisionSDKException) { + Log.e(TAG, e.message ?: "AIVisionSDKException occurred") + } finally { + } + } + } + + private fun configure() { + try { + if (uiState.value.usecaseSelected == UsecaseState.OCRBarcodeFind.value) { + //Swap the values as the presented index is reverse of what model expects + val processorOrder = + when (uiState.value.ocrBarcodeFindSettings.commonSettings.processorSelectedIndex) { + 0 -> arrayOf(2, 0, 1) // AUTO + 1 -> arrayOf(2) // DSP + 2 -> arrayOf(1) // GPU + 3 -> arrayOf(0) //CPU + else -> { + arrayOf(2, 0, 1) + } + } + textOCRSettings.detectionInferencerOptions.runtimeProcessorOrder = processorOrder + textOCRSettings.recognitionInferencerOptions.runtimeProcessorOrder = processorOrder + + textOCRSettings.decodingTotalProbThreshold = 0F + + textOCRSettings.detectionInferencerOptions.defaultDims.width = + uiState.value.ocrBarcodeFindSettings.commonSettings.inputSizeSelected + textOCRSettings.detectionInferencerOptions.defaultDims.height = + uiState.value.ocrBarcodeFindSettings.commonSettings.inputSizeSelected + } else { + //Swap the values as the presented index is reverse of what model expects + val processorOrder = + when (uiState.value.textOCRSettings.commonSettings.processorSelectedIndex) { + 0 -> arrayOf(2, 0, 1) // AUTO + 1 -> arrayOf(2) // DSP + 2 -> arrayOf(1) // GPU + 3 -> arrayOf(0) //CPU + else -> { + arrayOf(2, 0, 1) + } + } + textOCRSettings.detectionInferencerOptions.runtimeProcessorOrder = processorOrder + textOCRSettings.recognitionInferencerOptions.runtimeProcessorOrder = processorOrder + + textOCRSettings.decodingTotalProbThreshold = 0F + + textOCRSettings.detectionInferencerOptions.defaultDims.width = + uiState.value.textOCRSettings.commonSettings.inputSizeSelected + textOCRSettings.detectionInferencerOptions.defaultDims.height = + uiState.value.textOCRSettings.commonSettings.inputSizeSelected + + //Detection Parameters + textOCRSettings.heatmapThreshold = + uiState.value.textOCRSettings.advancedOCRSetting.heatmapThreshold.toFloat() + textOCRSettings.boxThreshold = + uiState.value.textOCRSettings.advancedOCRSetting.boxThreshold.toFloat() + textOCRSettings.minBoxArea = + uiState.value.textOCRSettings.advancedOCRSetting.minBoxArea.toInt() + textOCRSettings.minBoxSize = + uiState.value.textOCRSettings.advancedOCRSetting.minBoxSize.toInt() + textOCRSettings.unclipRatio = + uiState.value.textOCRSettings.advancedOCRSetting.unclipRatio.toFloat() + textOCRSettings.minRatioForRotation = + uiState.value.textOCRSettings.advancedOCRSetting.minRatioForRotation.toFloat() + + textOCRSettings.decodingMaxWordCombinations = + uiState.value.textOCRSettings.advancedOCRSetting.maxWordCombinations.toInt() + textOCRSettings.decodingTopkIgnoreCutoff = + uiState.value.textOCRSettings.advancedOCRSetting.topkIgnoreCutoff.toInt() + textOCRSettings.decodingTotalProbThreshold = + uiState.value.textOCRSettings.advancedOCRSetting.totalProbabilityThreshold.toFloat() + + // OCR Tiling related + if (uiState.value.textOCRSettings.advancedOCRSetting.enableTiling) { + textOCRSettings.tiling.enable = true + textOCRSettings.tiling.topCorrelationThr = + uiState.value.textOCRSettings.advancedOCRSetting.topCorrelationThreshold.toFloat() + textOCRSettings.tiling.mergePointsCutoff = + uiState.value.textOCRSettings.advancedOCRSetting.mergePointsCutoff.toInt() + textOCRSettings.tiling.splitMarginFactor = + uiState.value.textOCRSettings.advancedOCRSetting.splitMarginFactor.toFloat() + textOCRSettings.tiling.aspectRatioLowerThr = + uiState.value.textOCRSettings.advancedOCRSetting.aspectRatioLowerThreshold.toFloat() + textOCRSettings.tiling.aspectRatioUpperThr = + uiState.value.textOCRSettings.advancedOCRSetting.aspectRatioUpperThreshold.toFloat() + textOCRSettings.tiling.topkMergedPredictions = + uiState.value.textOCRSettings.advancedOCRSetting.topKMergedPredictions.toInt() + } else { + textOCRSettings.tiling.enable = false + } + if (uiState.value.textOCRSettings.advancedOCRSetting.enableGrouping) { + textOCRSettings.grouping.widthDistanceRatio = + uiState.value.textOCRSettings.advancedOCRSetting.widthDistanceRatio.toFloat() + textOCRSettings.grouping.heightDistanceRatio = + uiState.value.textOCRSettings.advancedOCRSetting.heightDistanceRatio.toFloat() + textOCRSettings.grouping.centerDistanceRatio = + uiState.value.textOCRSettings.advancedOCRSetting.centerDistanceRatio.toFloat() + textOCRSettings.grouping.paragraphHeightDistance = + uiState.value.textOCRSettings.advancedOCRSetting.paragraphHeightDistance.toFloat() + textOCRSettings.grouping.paragraphHeightRatioThreshold = + uiState.value.textOCRSettings.advancedOCRSetting.paragraphHeightRatioThreshold.toFloat() + } else { + //Reset to default values + textOCRSettings.grouping.widthDistanceRatio = 1.5f + textOCRSettings.grouping.heightDistanceRatio = 2.0f + textOCRSettings.grouping.centerDistanceRatio = 0.6f + textOCRSettings.grouping.paragraphHeightDistance = 1.0f + textOCRSettings.grouping.paragraphHeightRatioThreshold = 0.3333f + } + } + } catch (e: Exception) { + Log.e(TAG, "Fatal error: configure failed - ${e.message}") + } + } + + private fun updateOcrModelDemoReady(isReady: Boolean) { + viewModel.updateOcrModelDemoReady(isReady = isReady) + } + + private fun onDetectionTextResultWordLevel(entityList: List) { + val outputOCRResultData = mutableListOf() + entityList.forEach { entity -> + val paragraphEntity = entity + val lines = paragraphEntity.lines + for (line in lines) { + for (word in line.words) { + val bbox = word.complexBBox + + if (bbox != null && bbox.x != null && bbox.y != null && bbox.x.size == 4 && bbox.y.size == 4) { + val minX = bbox.x[0] + val maxX = bbox.x[2] + val minY = bbox.y[0] + val maxY = bbox.y[2] + + val rect = Rect(minX.toInt(), minY.toInt(), maxX.toInt(), maxY.toInt()) + val decodedValue = word.text + outputOCRResultData.add( + ResultData( + boundingBox = rect, + text = decodedValue + ) + ) + } + } + } + } + viewModel.updateOcrResultData( + results = FilterUtils.getOcrFilteredResultData( + uiState = uiState.value, + outputOCRResultData = outputOCRResultData + ) + ) + } + + private fun onDetectionTextResultLineLevel(entityList: List) { + val outputOCRResultData = mutableListOf() + entityList.forEach { entity -> + val paragraphEntity = entity + val lines = paragraphEntity.lines + for (line in lines) { + val bbox = line.complexBBox + + if (bbox != null && bbox.x != null && bbox.y != null && bbox.x.size == 4 && bbox.y.size == 4) { + val minX = bbox.x[0] + val maxX = bbox.x[2] + val minY = bbox.y[0] + val maxY = bbox.y[2] + + val rect = Rect(minX.toInt(), minY.toInt(), maxX.toInt(), maxY.toInt()) + val decodedValue = line.text + outputOCRResultData.add( + ResultData( + boundingBox = rect, + text = decodedValue + ) + ) + } + } + } + viewModel.updateOcrResultData( + results = FilterUtils.getOcrFilteredResultData( + uiState = uiState.value, + outputOCRResultData = outputOCRResultData + ) + ) + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Color.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Color.kt new file mode 100644 index 0000000..4b0fb3b --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.zebra.aidatacapturedemo.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Theme.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Theme.kt new file mode 100644 index 0000000..ee67228 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Theme.kt @@ -0,0 +1,50 @@ +package com.zebra.aidatacapturedemo.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import com.zebra.aidatacapturedemo.ui.view.Variables + +private val DarkColorScheme = darkColorScheme( + surface = Variables.surfaceDefault, + onSurface = Variables.mainDefault, + primary = Variables.mainPrimary, + onPrimary = Variables.borderPrimaryMain +) + +private val LightColorScheme = lightColorScheme( + surface = Variables.surfaceDefault, + onSurface = Variables.mainDefault, + primary = Variables.mainPrimary, + onPrimary = Variables.borderPrimaryMain +) + +@Composable +fun AIDataCaptureDemoTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Type.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Type.kt new file mode 100644 index 0000000..b2d0196 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Type.kt @@ -0,0 +1,21 @@ +package com.zebra.aidatacapturedemo.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import com.zebra.aidatacapturedemo.R + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontSize = 14.4.sp, + lineHeight = 21.6.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_regular)), + fontWeight = FontWeight(500), + color = Color(0xFF1D1E23), + ) +) \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureDemoApp.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureDemoApp.kt new file mode 100644 index 0000000..135c279 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureDemoApp.kt @@ -0,0 +1,510 @@ +// Copyright (c) 2024-2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved. + +package com.zebra.aidatacapturedemo.ui.view + +import android.content.Context +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.DrawerState +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonColors +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.Lifecycle +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.data.OcrRegularFilterOption +import com.zebra.aidatacapturedemo.data.UsecaseState +import com.zebra.aidatacapturedemo.ui.view.Variables.mainDefault +import com.zebra.aidatacapturedemo.ui.view.Variables.mainInverse +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +private const val TAG = "AIDataCaptureDemoApp" + +/** + * AIDataCaptureDemoApp.kt is the main entry point for the AI Data Capture Demo application. + * It defines the overall structure of the app's UI using Jetpack Compose. + * The file includes the main Scaffold, which contains a TopAppBar and a content area that hosts + * the navigation drawer and different screens based on user interactions. + * The TopAppBar is customized to show different icons and options depending on the current screen + * and use case selected by the user. The app also manages state using a ViewModel, allowing for + * reactive updates to the UI as users interact with it. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AIDataCaptureDemoAppBar( + uiState: AIDataCaptureDemoUiState, + viewModel: AIDataCaptureDemoViewModel, + navController: NavHostController, + context: Context, + scope: CoroutineScope, + drawerState: DrawerState +) { + CenterAlignedTopAppBar( + title = { + if (uiState.activeScreen == Screen.Preview) { + "" + } else { + AIDataCaptureDemoAppBarTitle(viewModel, uiState) + } + }, + navigationIcon = { + if (uiState.activeScreen == Screen.OCRBarcodeCapture) { + RectangularSingleChoiceSegmentedButton( + uiState.allBarcodeOCRCaptureFilter, + onChoiceSelected = { viewModel.updateAllBarcodeOCRCaptureFilter(it) }) + } else { + if ((uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value) && (uiState.isCaptureOrLiveEnabled == 0) && (uiState.activeScreen == Screen.Preview)) { + // Show different TopBar per Ux requirement for OCRBarcodeFind Usecase Image Capture mode. + // Add round close button to actions of the TopAppBar and remove navigation icon by passing empty composable here. + EmptyComposable() + } else { + IconButton(onClick = { + if (uiState.activeScreen == Screen.Start) { + scope.launch { + drawerState.open() + } + } else { + viewModel.handleBackButton(navController) + } + }) { + if (uiState.activeScreen == Screen.Start) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.hamburger_icon), + contentDescription = stringResource(R.string.home_button) + ) + } else { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = stringResource(R.string.back_button) + ) + } + } + } + } + }, + colors = if (uiState.activeScreen == Screen.Preview) { + TopAppBarDefaults.mediumTopAppBarColors( + containerColor = Color.Transparent.copy(alpha = 0.3F), + titleContentColor = Color.Transparent, + navigationIconContentColor = mainInverse, + actionIconContentColor = mainInverse + ) + } else { + TopAppBarDefaults.mediumTopAppBarColors( + containerColor = mainDefault, + titleContentColor = mainInverse, + navigationIconContentColor = mainInverse, + actionIconContentColor = mainInverse + ) + }, + actions = { + + if (uiState.usecaseSelected == UsecaseState.OCRBarcodeFind.value) { + var isFilterIconClicked by remember { mutableStateOf(false) } + + val isOcrDefaultFilterSelected = + uiState.ocrFilterData.selectedRegularFilterOption == OcrRegularFilterOption.UNFILTERED + val isBarcodeDefaultFilterSelected = + uiState.barcodeFilterData.selectedAdvancedFilterOptionList.isEmpty() + if (uiState.isCaptureOrLiveEnabled == 0) { + // Camera Capture flow + + if (uiState.activeScreen == Screen.DemoStart) { + IconButton(onClick = { + isFilterIconClicked = true + }) { + Icon( + ImageVector.vectorResource( + id = if (isOcrDefaultFilterSelected && isBarcodeDefaultFilterSelected) { + R.drawable.ic_filter_default + } else { + R.drawable.ic_filter_selected + } + ), + contentDescription = "Filter icon description", + tint = Color.Unspecified + ) + } + } + if (uiState.activeScreen == Screen.OCRBarcodeResults) { + Image( + painter = painterResource(id = R.drawable.ic_trash_can), + contentDescription = "image description", + contentScale = ContentScale.None, + modifier = Modifier.Companion + .padding(Variables.spacingMedium) + .clickable { + viewModel.clearOcrBarcodeCaptureSession() + navController.navigate(route = Screen.Preview.route) { + popUpTo("preview_screen") { + inclusive = true + } + launchSingleTop = true // Prevents multiple copies of the same destination at the top of the stack + } + } + ) + } + if ((uiState.activeScreen == Screen.Preview) || (uiState.activeScreen == Screen.OCRBarcodeCapture)) { + // Show different TopBar per Ux requirement for OCRBarcodeFind Usecase Image Capture mode. + // Add round close button to actions of the TopAppBar + RoundCloseButton(onClick = { + scope.launch { + viewModel.handleBackButton(navController) + } + }) + } else { + EmptyComposable() + } + } else { + // Live Camera flow + + // filter icon + if (uiState.activeScreen == Screen.DemoStart || uiState.activeScreen == Screen.Preview) { + IconButton(onClick = { + isFilterIconClicked = true + }) { + Icon( + ImageVector.vectorResource( + id = if (isOcrDefaultFilterSelected && isBarcodeDefaultFilterSelected) { + R.drawable.ic_filter_default + } else { + R.drawable.ic_filter_selected + } + ), + contentDescription = "Filter icon description", + tint = Color.Unspecified + ) + } + } + + // mic icon + if (uiState.activeScreen == Screen.Preview && uiState.isOCRModelEnabled) { + IconButton(onClick = { + if (FeedbackUtils.micStatePressed == false) { + FeedbackUtils.micStatePressed = true + FeedbackUtils.startListening(uiState) + } + }) { + Icon( + ImageVector.Companion.vectorResource(R.drawable.mic_icon), + contentDescription = "Microphone" + ) + } + } + + } + + if (isFilterIconClicked) { + FilterOptionsDropdownMenu( + isOcrDefaultFilterSelected = isOcrDefaultFilterSelected, + isBarcodeDefaultFilterSelected = isBarcodeDefaultFilterSelected, + onMenuDismissed = { + isFilterIconClicked = false + }, + onOCRFilterOptionSelected = { + isFilterIconClicked = false + navController.navigate(route = Screen.OCRFindFilterHome.route) + }, + onBarcodeFilterOptionSelected = { + isFilterIconClicked = false + navController.navigate(route = Screen.BarcodeFindFilterHome.route) + }) + } + } + if (uiState.activeScreen == Screen.DemoStart) { + IconButton(onClick = { + navController.navigate(route = Screen.DemoSetting.route) + }) { + Icon( + ImageVector.Companion.vectorResource(id = R.drawable.settings_icon), + contentDescription = "Settings" + ) + } + } + }) +} +//describe the app's UI +/** + * Main entry point for the application, that + */ +@Composable +fun AIDataCaptureDemoApp( + viewModel: AIDataCaptureDemoViewModel, + activityInnerPadding: PaddingValues, + activityLifecycle: Lifecycle +) { + val navController = rememberNavController() + val uiState by viewModel.uiState.collectAsState() + val context = LocalContext.current + viewModel.restoreDefaultSettings() + viewModel.clearOcrBarcodeCaptureSession() + viewModel.updateAppBarTitle(stringResource(id = R.string.app_name)) + + val scope = rememberCoroutineScope() + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + var selectedItem by remember { mutableStateOf("Home") } + + Scaffold( + topBar = { + if ((uiState.usecaseSelected == UsecaseState.Product.value || uiState.usecaseSelected == UsecaseState.OCR.value) && + (uiState.activeScreen == Screen.Preview || uiState.activeScreen == Screen.ProductsCapture) + ) { + // Don't show the TopBar here for Product Recognition Use case Preview & Capture Screen + } else { + AIDataCaptureDemoAppBar( + uiState = uiState, + viewModel = viewModel, + navController = navController, + context = context, + scope = scope, + drawerState = drawerState + ) + } + }, + content = { innerPadding -> + AIDataCaptureModalNavigationDrawer( + drawerState = drawerState, + innerPadding = innerPadding, + activityInnerPadding = activityInnerPadding, + selectedItem = selectedItem, + onSelectedItemValuesChange = { selectedItem = it }, + scope = scope, + navController = navController, + viewModel = viewModel, + context = context, + activityLifecycle = activityLifecycle + ) + }, + containerColor = Variables.surfaceDefault + ) +} + +@Composable +fun AIDataCaptureDemoAppBarTitle( + viewModel: AIDataCaptureDemoViewModel, + uiState: AIDataCaptureDemoUiState +) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(color = Variables.mainDefault) + .padding(start = 8.dp, top = 8.dp, end = 8.dp, bottom = 8.dp) + ) { + Text( + text = uiState.appBarTitle, + softWrap = true, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + style = TextStyle( + fontSize = 18.sp, + lineHeight = 28.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = mainInverse, + ) + ) + } +} + +@Composable +fun RectangularSingleChoiceSegmentedButton( + selectedIndex: Int, + onChoiceSelected: (Int) -> Unit +) { + val options = listOf("All", "Barcode", "OCR") + SingleChoiceSegmentedButtonRow( + modifier = Modifier + .width(240.dp) + .padding(1.dp) + .background(Variables.backgroundDark) + .border( + width = 1.dp, + color = Variables.colorsMainSubtle, + shape = RoundedCornerShape(8.dp) + ) + ) { + options.forEachIndexed { index, label -> + SegmentedButton( + shape = RoundedCornerShape(8.dp), + colors = SegmentedButtonColors( + Variables.blackText, + Color.White, + Variables.blackText, + inactiveContainerColor = Variables.backgroundDark, + inactiveContentColor = Variables.colorsMainSubtle, + inactiveBorderColor = Variables.backgroundDark, + disabledActiveContainerColor = Variables.backgroundDark, + disabledActiveContentColor = Variables.colorsMainSubtle, + disabledActiveBorderColor = Variables.backgroundDark, + disabledInactiveContainerColor = Variables.backgroundDark, + disabledInactiveContentColor = Variables.colorsMainSubtle, + disabledInactiveBorderColor = Variables.backgroundDark, + ), + modifier = Modifier + .wrapContentWidth() + .padding(1.dp), + onClick = { onChoiceSelected(index) }, + selected = index == selectedIndex, + label = { Text(label, color = Color.White) }, + icon = {} + ) + } + } +} + +@Composable +fun FilterOptionsDropdownMenu( + onMenuDismissed: () -> Unit, + onOCRFilterOptionSelected: () -> Unit, + onBarcodeFilterOptionSelected: () -> Unit, + isOcrDefaultFilterSelected: Boolean, + isBarcodeDefaultFilterSelected: Boolean +) { + Box( + modifier = Modifier + .wrapContentSize(Alignment.BottomStart) + .background( + color = Variables.surfaceDefault + ) + .border( + width = 1.dp, + color = Variables.borderDefault, + shape = RoundedCornerShape(size = Variables.radiusRounded) + ) + ) { + DropdownMenu( + expanded = true, + onDismissRequest = { + onMenuDismissed() + }, + offset = DpOffset(x = (-10).dp, y = 10.dp), // Move the Menu 10dp left and 10dp bottom + modifier = Modifier + .background(color = Variables.surfaceDefault) + ) { + // "OCR Filters" option + DropdownMenuItem( + text = { + Text( + text = "OCR Filters", + style = TextStyle( + fontSize = 14.sp, + lineHeight = 20.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextDefault, + ) + ) + }, + onClick = { + onOCRFilterOptionSelected() + }, + leadingIcon = { + if (isOcrDefaultFilterSelected) { + Icon( + ImageVector.vectorResource(R.drawable.ic_menu_ocr), + contentDescription = "menu ocr", + tint = Variables.blackText + ) + } else { + Icon( + ImageVector.vectorResource(R.drawable.ic_ocr_filter_selected), + contentDescription = "menu ocr", + tint = Color.Unspecified + ) + } + } + ) + + // "Barcode Filters" option + DropdownMenuItem( + text = { + Text( + text = "Barcode Filters", + style = TextStyle( + fontSize = 14.sp, + lineHeight = 20.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.colorsTextDefault, + ) + ) + }, + onClick = { + onBarcodeFilterOptionSelected() + }, + leadingIcon = { + if (isBarcodeDefaultFilterSelected) { + Icon( + ImageVector.vectorResource(R.drawable.ic_menu_barcode), + contentDescription = "menu barcode", + tint = Variables.blackText + ) + } else { + Icon( + ImageVector.vectorResource(R.drawable.ic_barcode_filter_selected), + contentDescription = "menu barcode", + tint = Color.Unspecified + ) + } + } + ) + } + } +} diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureModalNavigationDrawer.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureModalNavigationDrawer.kt new file mode 100644 index 0000000..8522a02 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureModalNavigationDrawer.kt @@ -0,0 +1,250 @@ +package com.zebra.aidatacapturedemo.ui.view + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.DrawerState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.NavigationDrawerItemColors +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.Lifecycle +import androidx.navigation.NavHostController +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Composable function that sets up a Modal Navigation Drawer for the AI Data Capture Demo app. + * + * @param drawerState The state of the navigation drawer. + * @param innerPadding The padding values for the content inside the drawer. + * @param activityInnerPadding The padding values for the main activity content. + * @param selectedItem The currently selected item in the navigation drawer. + * @param onSelectedItemValuesChange Callback to update the selected item state. + * @param scope CoroutineScope for launching coroutines, such as closing the drawer. + * @param navController NavHostController for handling navigation within the app. + * @param viewModel The ViewModel instance for managing UI-related data and logic. + * @param context The Context of the current state of the application, used for accessing resources and starting activities. + * @param activityLifecycle The Lifecycle of the activity, used for managing lifecycle-aware components. + */ +@Composable +fun AIDataCaptureModalNavigationDrawer( + drawerState: DrawerState, + innerPadding: PaddingValues, + activityInnerPadding: PaddingValues, + selectedItem: String, + onSelectedItemValuesChange: (String) -> Unit, + scope: CoroutineScope, + navController: NavHostController, + viewModel: AIDataCaptureDemoViewModel, + context: Context, + activityLifecycle: Lifecycle +) { + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + ModalDrawerSheet( + modifier = Modifier + .fillMaxWidth(0.75f) // Set the width to 75% + .padding( + top = innerPadding.calculateTopPadding() - activityInnerPadding.calculateTopPadding(), + bottom = innerPadding.calculateBottomPadding() + ), + drawerShape = RectangleShape, + drawerContainerColor = Variables.surfaceTertiary, + ) { + NavigationDrawerItem( + label = { + Text( + text = "Home", + + // Standard/Title Small + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.inverseDefault + ) + ) + }, + selected = selectedItem == "Home", + onClick = { + onSelectedItemValuesChange("Home") + scope.launch { drawerState.close() } + }, + icon = { + Icon( + Icons.Default.Home, + contentDescription = "Home", + tint = Variables.inverseDefault + ) + }, + shape = RectangleShape, + modifier = Modifier + .height(40.dp) + .padding(horizontal = 0.dp, vertical = 0.dp), + colors = getNavigationDrawerItemColor() + ) + NavigationDrawerItem( + label = { + Text( + text = stringResource(R.string.about), + + // Standard/Title Small + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.inverseDefault + ) + ) + }, + selected = selectedItem == stringResource(R.string.about), + onClick = { + onSelectedItemValuesChange(context.getString(R.string.about)) + scope.launch { drawerState.close() } + }, + icon = { + Icon( + Icons.Default.Info, + contentDescription = "Info", + tint = Variables.inverseDefault + ) + }, + shape = RectangleShape, + modifier = Modifier.height(40.dp), + colors = getNavigationDrawerItemColor() + ) + NavigationDrawerItem( + label = { + Text( + text = "Send Feedback", + + // Standard/Title Small + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.inverseDefault + ) + ) + }, + selected = selectedItem == "Send Feedback", + onClick = { + onSelectedItemValuesChange("Send Feedback") + scope.launch { drawerState.close() } + + openFeedbackUrl(context = context) + }, + icon = { + Icon( + ImageVector.Companion.vectorResource(R.drawable.satisfied_icon), + contentDescription = "Send Feedback", + tint = Variables.inverseDefault + ) + }, + shape = RectangleShape, + modifier = Modifier.height(40.dp), + colors = getNavigationDrawerItemColor() + ) + + Spacer(Modifier.weight(1f)) + HorizontalDivider(thickness = 0.25.dp, color = Variables.textSubtle) + Spacer(modifier = Modifier.height(12.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + ) { + Icon( + imageVector = ImageVector.Companion.vectorResource(R.drawable.zebra_logo_icon), + contentDescription = "App Information", + tint = Variables.surfaceDefault + ) + Text( + text = "Powered by Zebra Frontline AI Enablers", + style = TextStyle( + fontSize = 12.sp, + lineHeight = 16.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.textSubtle, + textAlign = TextAlign.Center, + ) + ) +// Spacer(modifier = Modifier.height(14.dp)) + + } + } + }, + // This is the main content of the screen that the drawer will slide over. + content = { + if (selectedItem == "About") { + AboutScreen(innerPadding = innerPadding) + } else { + NavigationStack( + navController, + viewModel, + activityInnerPadding = activityInnerPadding, + innerPadding, + context, + activityLifecycle + ) + } + } + ) +} + +@Composable +private fun getNavigationDrawerItemColor(): NavigationDrawerItemColors { + return NavigationDrawerItemDefaults.colors( + selectedBadgeColor = Variables.mainInverse, + unselectedBadgeColor = Variables.surfaceTertiary, + selectedContainerColor = Variables.surfaceTertiarySelected, + unselectedContainerColor = Variables.surfaceTertiary, + selectedIconColor = Variables.inverseDefault, + unselectedIconColor = Variables.mainLight, + selectedTextColor = Variables.mainInverse, + unselectedTextColor = Variables.mainLight + ) +} + +private fun openFeedbackUrl(context: Context) { + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://app.smartsheet.com/b/form/da3b9fb25b88495cbca59a4470d7b186") + ) + context.startActivity(intent) +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureStartScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureStartScreen.kt new file mode 100644 index 0000000..61d2aac --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureStartScreen.kt @@ -0,0 +1,407 @@ +package com.zebra.aidatacapturedemo.ui.view + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat.getString +import androidx.navigation.NavController +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +/** + * Composable function for the AI Data Capture Start Screen. + * + * This screen displays a list of use case demos and technology demos in an expandable format. + * Users can click on each item to navigate to the respective demo screen. + * + * @param viewModel The ViewModel that holds the state and logic for the AI Data Capture Demo. + * @param navController The NavController used for navigation between screens. + * @param innerPadding The padding values to be applied to the content of the screen. + */ +@Composable +fun AIDataCaptureStartScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + innerPadding: PaddingValues +) { + AnimateExpandableList(viewModel, navController, innerPadding) +} + +data class ExpandableItem( + val iconId: Int, + val title: String, + var isExpanded: Boolean = false +) + +@Composable +fun AnimateExpandableList( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + innerPadding: PaddingValues +) { + val itemsTitle = arrayOf("Use Case Demos", "Technology Demos") + val itemsIcon = arrayOf(R.drawable.usecase_icon, R.drawable.technology_icon) + + val items = + remember { List(2) { index -> ExpandableItem(itemsIcon[index], itemsTitle[index]) } } + + val expandedStates = + remember { mutableStateListOf(*BooleanArray(items.size) { true }.toTypedArray()) } + val listState = rememberLazyListState() + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .border(width = 1.dp, color = Variables.borderDefault) + .fillMaxWidth() + .background(color = Variables.surfaceDefault) + .height(48.dp), + horizontalAlignment = Alignment.Start, + state = listState + ) { + itemsIndexed(items, key = { index, _ -> index }) { index, item -> + ExpandableListItem( + item = item, + index = index, + isExpanded = expandedStates[index], + onExpandedChange = { + for (i in items.indices) { + expandedStates[i] = false + } + expandedStates[index] = it + }, + viewModel, navController + ) + } + } +} + +@Composable +fun ExpandableListItem( + item: ExpandableItem, + index: Int, + isExpanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + viewModel: AIDataCaptureDemoViewModel, + navController: NavController +) { + val interactionSource = remember { MutableInteractionSource() } + val rotationAngle by animateFloatAsState(targetValue = if (isExpanded) 180f else 0f) + + Column( + horizontalAlignment = Alignment.Start, + modifier = Modifier + .fillMaxWidth() + .background(color = Variables.surfaceDefault) + .clickable(interactionSource = interactionSource, indication = null) { + onExpandedChange(!isExpanded) + } + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .border(width = 1.dp, color = Variables.borderDefault) + .fillMaxWidth() + .wrapContentHeight() + .background(color = Variables.mainInverse) + .padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp) + ) { + Icon( + imageVector = ImageVector.Companion.vectorResource(item.iconId), + contentDescription = null, + modifier = Modifier + .padding(1.dp) + .width(24.dp) + .height(24.dp), + tint = Variables.mainSubtle + ) + Text( + text = item.title, + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Variables.mainDefault, + ) + ) + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = ImageVector.Companion.vectorResource(id = R.drawable.down_arrow_icon), + contentDescription = if (isExpanded) "Collapse" else "Expand", + modifier = Modifier + .graphicsLayer(rotationZ = rotationAngle) + .padding(1.dp) + .width(20.dp) + .height(20.dp), + tint = Variables.mainSubtle + ) + } + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut(), + + ) { + if (item.title.equals("Use Case Demos")) { + AIDataCaptureUsecaseList(viewModel, navController) + } else if (item.title.equals("Technology Demos")) { + AIDataCaptureTechnologyList(viewModel, navController) + } + } + } +} + +@Composable +fun AIDataCaptureUsecaseList(viewModel: AIDataCaptureDemoViewModel, navController: NavController) { + val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + Column( + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top), + horizontalAlignment = Alignment.Start, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(16.dp) + ) { + AIDataCaptureListItem( + R.drawable.ocr_finder_icon, + stringResource(id = R.string.ocr_barcode_find), + stringResource(id = R.string.ocr_barcode_find_desc), + Variables.mainIcon1, + Variables.secondaryIcon1, + onItemClick = { selectedUsecase -> + viewModel.updateAppBarTitle(getString(context, R.string.ocr_barcode_find)) + viewModel.updateSelectedUsecase(selectedUsecase) + viewModel.initModel() + navController.navigate(route = Screen.DemoStart.route) + }) + AIDataCaptureListItem( + R.drawable.product_enrollment_recognition_icon, + stringResource(id = R.string.product_enrollment_recognition_demo), + stringResource(id = R.string.product_enrollment_recognition_desc), + Variables.mainIcon1, + Variables.secondaryIcon1, + onItemClick = { selectedUsecase -> + viewModel.updateAppBarTitle( + getString( + context, + R.string.product_enrollment_recognition_demo + ) + ) + viewModel.updateSelectedUsecase(selectedUsecase) + viewModel.initModel() + navController.navigate(route = Screen.DemoStart.route) + }) + } +} + +@Composable +fun AIDataCaptureTechnologyList( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController +) { + val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + Column( + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top), + horizontalAlignment = Alignment.Start, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(16.dp) + ) { + AIDataCaptureListItem( + R.drawable.ocr_icon, + stringResource(id = R.string.ocr_demo), + stringResource(id = R.string.ocr_desc), + Variables.mainIcon2, + Variables.secondaryIcon2, + onItemClick = { selectedUsecase -> + viewModel.updateAppBarTitle(getString(context, R.string.ocr_demo)) + viewModel.updateSelectedUsecase(selectedUsecase) + viewModel.initModel() + navController.navigate(route = Screen.DemoStart.route) + }) + AIDataCaptureListItem( + R.drawable.barcode_icon, + stringResource(id = R.string.barcode_demo), + stringResource(id = R.string.barcode_desc), + Variables.mainIcon2, + Variables.secondaryIcon2, + onItemClick = { selectedUsecase -> + viewModel.updateAppBarTitle(getString(context, R.string.barcode_demo)) + viewModel.updateSelectedUsecase(selectedUsecase) + viewModel.initModel() + navController.navigate(route = Screen.DemoStart.route) + }) + AIDataCaptureListItem( + R.drawable.barcode_icon, + stringResource(id = R.string.barcode_map_demo), + stringResource(id = R.string.barcode_map_desc), + Variables.mainIcon2, + Variables.secondaryIcon2, + onItemClick = { selectedUsecase -> + viewModel.updateAppBarTitle(getString(context, R.string.barcode_map_demo)) + viewModel.updateSelectedUsecase(selectedUsecase) + viewModel.initModel() + navController.navigate(route = Screen.DemoStart.route) + }) + AIDataCaptureListItem( + R.drawable.retail_shelf_icon, + stringResource(id = R.string.retail_shelf_demo), + stringResource(id = R.string.retail_shelf_desc), + Variables.mainIcon2, + Variables.secondaryIcon2, + onItemClick = { selectedUsecase -> + viewModel.updateAppBarTitle(getString(context, R.string.retail_shelf_demo)) + viewModel.updateSelectedUsecase(selectedUsecase) + viewModel.initModel() + navController.navigate(route = Screen.DemoStart.route) + }) + } +} + +@Composable +fun AIDataCaptureListItem( + resId: Int, + title: String, + description: String, + mainColor: Color, + secondaryColor: Color, + onItemClick: (text: String) -> Unit +) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .shadow(4.dp, shape = RoundedCornerShape(12.dp)) + .fillMaxWidth() + .wrapContentHeight() + .background(color = Variables.surfaceDefault, shape = RoundedCornerShape(size = 16.dp)) + .padding(start = 8.dp, end = 8.dp) + .clickable { + onItemClick(title) + } + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .width(40.dp) + .height(40.dp) + .background( + shape = RoundedCornerShape(4.dp), + brush = Brush.verticalGradient( + colors = listOf( + mainColor, + secondaryColor + ) + ) + ) + ) { + Image( + painter = painterResource(id = resId), + contentDescription = "image description", + contentScale = ContentScale.Fit, + modifier = Modifier + .padding(1.dp) + .width(24.dp) + .height(24.dp) + ) + } + Column( + verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.Start, + modifier = Modifier + .fillMaxWidth() + .height(84.dp) + .padding(top = 12.dp, bottom = 12.dp) + ) { + Text( + text = title, + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Variables.mainDefault, + ) + ) + Text( + text = description, + style = TextStyle( + fontSize = 14.sp, + lineHeight = 18.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_regular)), + fontWeight = FontWeight(400), + color = Variables.mainSubtle, + ) + ) + } + } +} + +fun CustomRoundedCornerShape( + topStart: Dp = 0.dp, + topEnd: Dp = 0.dp, + bottomEnd: Dp = 0.dp, + bottomStart: Dp = 0.dp +) = RoundedCornerShape( + topStart = CornerSize(topStart), + topEnd = CornerSize(topEnd), + bottomEnd = CornerSize(bottomEnd), + bottomStart = CornerSize(bottomStart) +) \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AboutScreen.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AboutScreen.kt new file mode 100644 index 0000000..e37df1e --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AboutScreen.kt @@ -0,0 +1,630 @@ +package com.zebra.aidatacapturedemo.ui.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.zebra.aidatacapturedemo.BuildConfig +import com.zebra.aidatacapturedemo.R + +/** + * AboutScreen.kt is a composable function that defines the UI for the "About" screen of the + * AI Data Capture Demo application. It displays information about the app, including its version, + * the AI Suite SDK version, and an End User License Agreement (EULA). + * The screen is structured using a Column layout with multiple Rows to organize the content. + * An AlertDialog is used to show the EULA when the user clicks on the corresponding row. + * The design follows a consistent style with specific fonts, colors, and + * spacing to ensure a cohesive user experience. + * + * @param innerPadding PaddingValues that can be used to adjust the padding of the screen content + */ +@Composable +fun AboutScreen(innerPadding: PaddingValues) { + var showDialog by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .background(color = Variables.surfaceDefault) + ) { + Column( + modifier = Modifier + .padding(top = 12.dp) + .align(Alignment.TopStart) + ) { + // Row 1 + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .background(color = Variables.mainLight) + ) { + Text( + text = "About", + + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Variables.mainDefault, + ), + modifier = Modifier.padding(8.dp) + ) + } + + // Row 2 + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .background(color = Variables.mainInverse) + ) { + Text( + text = "Explore Zebra Frontline AI Enablers Data Capture SDK latest features and solutions for Zebra Android™ devices.", + + style = TextStyle( + fontSize = 14.sp, + lineHeight = 18.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Variables.mainDefault, + ), + modifier = Modifier.padding( + start = 12.dp, + end = 12.dp, + top = 16.dp, + bottom = 16.dp + ) + ) + } + + // Row 3 + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "AI Data Capture Demo", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Color(0xFF1D1E23), + ), + modifier = Modifier.padding(top = 10.dp, start = 14.4.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + val appVersion = BuildConfig.AI_DataCaptureDemo_Version + Text( + text = appVersion, + + style = TextStyle( + fontSize = 12.sp, + lineHeight = 16.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Color(0xFF646A78), + textAlign = TextAlign.Right, + ), + modifier = Modifier.padding(end = 22.4.dp, top = 14.dp) + ) + } + + // Row 4 + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "AI Suite SDK", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Color(0xFF1D1E23), + ), + modifier = Modifier.padding(top = 16.dp, start = 14.4.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + val aisdkVersion = BuildConfig.Zebra_AI_VisionSdk_Version + Text( + text = aisdkVersion, + + style = TextStyle( + fontSize = 12.sp, + lineHeight = 16.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Color(0xFF646A78), + textAlign = TextAlign.Right, + ), + modifier = Modifier.padding(top = 20.dp, end = 22.4.dp) + ) + } + + // Row 5 + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "End User License Agreement", + + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Color(0xFF1D1E23), + ), + modifier = Modifier.padding(start = 16.dp, top = 30.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + IconButton( + onClick = { + showDialog = true + }, + modifier = Modifier.padding(top = 30.dp, end = 12.dp) + ) { + Icon( + imageVector = ImageVector.Companion.vectorResource(R.drawable.icon_arrow_forward), + contentDescription = "Arrow Button", + tint = Variables.mainDefault + ) + } + } + + // Row 6 + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Copyright © 2025 Zebra Technologies Corporation and/or its affiliates. All rights reserved.", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Color(0xFF1D1E23), + ), + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + top = 36.dp, + bottom = 28.dp + ) + ) + } + + if (showDialog) { + AlertDialog( + onDismissRequest = { showDialog = false }, + title = { + Text( + text = "End User License Agreement (Restricted Software)", + + // Standard/Title Large + style = TextStyle( + fontSize = 20.sp, + lineHeight = 28.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans_medium)), + fontWeight = FontWeight(500), + color = Color(0xFF1D1E23), + textAlign = TextAlign.Center, + ) + ) + }, + text = { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + val eULAHtmlText = "
    \n" + + " This End User License Agreement (this “Agreement”) includes important information about your\n" + + " relationship with Zebra. Please read it carefully.\n" + + "

    \n" + + " 1          Introduction\n" + + "
    \n" + + " 1.1     This Agreement is a legal contract made between the person or entity\n" + + " agreeing to these terms and conditions (“you”) and Zebra Technologies Corporation (“Zebra”) that\n" + + " governs your use of software, firmware, application programming interfaces, user interfaces, and any\n" + + " other type of machine-readable instructions or code as provided by Zebra that accompany or reference\n" + + " this Agreement, along with any corresponding documentation (collectively, the “Software”).\n" + + "

    \n" + + " 1.2     By ordering, subscribing to, installing, executing, or otherwise using\n" + + " the Software, you (i) acknowledge that you have read and understand this Agreement, (ii) agree to be\n" + + " bound by this Agreement, (iii) confirm that you are lawfully able to enter into contracts, and (iv)\n" + + " if you are accepting this Agreement on behalf of an entity, such as an organization or business,\n" + + " confirm that you have the authority to bind that entity to this Agreement.\n" + + "

    \n" + + " 2          Term of this Agreement\n" + + "
    \n" + + " 2.1     This Agreement becomes effective on the earlier of (i) the date you\n" + + " accept this Agreement by, for example, clicking a button and (ii) the earliest date on which you\n" + + " install, execute, or otherwise use the Software, and ends upon termination in accordance with this\n" + + " section (“Term”).\n" + + "

    \n" + + " 2.2     This Agreement will automatically terminate without notice from Zebra\n" + + " upon your breach or violation of any term or condition of this Agreement.\n" + + "

    \n" + + " 2.3     If you are in possession of the Software pursuant to a subscription\n" + + " model or other type of commercial agreement, this Agreement shall terminate upon the expiration or\n" + + " termination of that subscription model or other type of commercial agreement.\n" + + "

    \n" + + " 2.4     Upon termination of this Agreement, you will immediately cease using the\n" + + " Software and delete (i) the Software, (ii) any other application provided to you by Zebra for\n" + + " purposes of interacting with the Software, and (iii) any Zebra Content (as defined below) obtained\n" + + " through your use of the Software.\n" + + "

    \n" + + " 2.5     You may terminate this Agreement by ceasing all use of the Software and\n" + + " deleting the Software from your devices.\n" + + "

    \n" + + " 2.6     Sections 4, 6, 8, and 11-14 will survive the termination or expiration\n" + + " of this Agreement.\n" + + "

    \n" + + " 3          License and Ownership\n" + + "
    \n" + + " 3.1     Subject to your compliance with this Agreement, Zebra grants you a\n" + + " limited, revocable, non-exclusive, non-sublicensable license to, during the Term, use the Software\n" + + " solely for your internal business purposes and, for Software delivered with Zebra hardware, solely\n" + + " in support of Zebra hardware.\n" + + "

    \n" + + " 3.2     The Software is licensed; not sold. Zebra reserves all right, title, and\n" + + " interest not expressly granted to you in this Agreement. Zebra or its licensors or suppliers own the\n" + + " title, copyright, and other intellectual property rights in the Software and certain Content\n" + + " associated therewith.\n" + + "

    \n" + + " 4          Restrictions\n" + + "
    \n" + + " 4.1     You shall not or permit another to modify, distribute, publicly display,\n" + + " publicly perform, or create derivative works of the Software.\n" + + "

    \n" + + " 4.2     You shall not or permit another to disassemble, decompile,\n" + + " reverse-engineer, or attempt to discover or derive the source code of the Software, except and only\n" + + " to the extent that such activity is expressly permitted by applicable law not withstanding this\n" + + " limitation.\n" + + "

    \n" + + " 4.3     You shall not or permit another to rent, sell, lease, lend, sublicense,\n" + + " provide commercial hosting services involving the Software, or in any other way allow third parties\n" + + " to exploit the Software.\n" + + "

    \n" + + " 4.4     You shall not or permit another to modify, circumvent, deactivate,\n" + + " degrade or thwart any software-based or hardware-based protection mechanism Zebra has in place to\n" + + " safeguard the Software.\n" + + "

    \n" + + " 4.5     The rights granted to you hereunder are associated with you and cannot\n" + + " be used or otherwise applied to anyone other than you. Unless made in connection with a sale of a\n" + + " device on which the Software is installed by or under the authorization of Zebra, you may not convey\n" + + " the Software to any third-party or permit any third party to do so.\n" + + "

    \n" + + " 4.6     You may not assign this Agreement or any rights or obligations\n" + + " hereunder, by operation of law or otherwise, without prior written consent from Zebra. Zebra may\n" + + " assign this Agreement and its rights and obligations without your consent. Subject to the foregoing,\n" + + " this Agreement shall be binding upon and inure to the benefit of the parties to it and their\n" + + " respective legal representatives, successors, and permitted assigns.\n" + + "

    \n" + + " 5          Zebra’s Approach to Privacy\n" + + "
    \n" + + " 5.1     Zebra’s Privacy Policy (located at: https://www.zebra.com/privacy
    ), as amended from\n" + + " time to time, is hereby incorporated by reference into this Agreement. If you submit personal data\n" + + " to Zebra in connection with your use of the Software, the ways in which Zebra collects and uses that\n" + + " data are regulated by Zebra’s Privacy Policy in accordance with applicable law.\n" + + "

    \n" + + " 5.2     Zebra is committed to General Data Protection Regulation (GDPR)\n" + + " compliance and Zebra’s GDPR Addendum (located at: https://www.zebra.com/GDPR\n" + + " supplements Zebra’s Privacy Policy.\n" + + "

    \n" + + " 6          Permissions\n" + + "
    \n" + + " 6.1     “Content” means image data, images, graphics, text, templates, formats,\n" + + " forms, digital certificates or other types of user-identifying packages, plug-ins, widgets, audio,\n" + + " video, and audiovisual data.\n" + + "

    \n" + + " 6.2     “Input” means data provided to Zebra, whether by you or another person\n" + + " using the Software, for use by the Software to provide a feature or functionality. Input includes\n" + + " Content, measurement values, readings, sensor outputs, calculation results, and instructions.\n" + + "

    \n" + + " 6.3     You acknowledge that if the Software requires access to non-Zebra\n" + + " hardware, non-Zebra software, or non-Zebra Content to perform a function or provide a feature and\n" + + " you deny such permission, the corresponding function or feature will not be available or execute\n" + + " properly.\n" + + "

    \n" + + " 6.4     Certain functions of the Software may require access to certain software\n" + + " and/or hardware. To the extent permission is required, you hereby grant Zebra permission to, during\n" + + " the Term, access all software incorporated into Zebra hardware as necessary for the Software to\n" + + " perform those functions.\n" + + "

    \n" + + " 6.5     You agree that any ideas, suggestions, comments, or reviews you provide\n" + + " to Zebra in relation to the Software (“Feedback”) is not confidential, and Zebra shall not have any\n" + + " obligation to treat Feedback as confidential information. You agree that Zebra is free to use\n" + + " Feedback to improve its products and services.\n" + + "

    \n" + + " 6.6     Where applicable, you agree to waive and not enforce any “moral rights”\n" + + " or equivalent rights you have in Feedback, Input, or your Content provided to Zebra in connection\n" + + " with the Software.\n" + + "

    \n" + + " 7          Updates and Fixes\n" + + "
    \n" + + " 7.1     Nothing in this Agreement entitles you to new releases of the Software.\n" + + " If Zebra, at its discretion, makes updates, fixes, or patches to the Software available during the\n" + + " Term without providing superseding terms, this Agreement applies to such updates, fixes, and\n" + + " patches.\n" + + "

    \n" + + " 7.2     Provided that the functionality and features of the Software remain\n" + + " substantially similar thereafter, Zebra may automatically update the Software without requiring your\n" + + " acceptance. Zebra will make reasonable efforts to provide you notice of any automatic updates made\n" + + " to the Software, although such notice is not required.\n" + + "

    \n" + + " 8          Data Collection\n" + + "
    \n" + + " 8.1     “Anonymized Data” means data that cannot be used to identify you or any\n" + + " other person.\n" + + "

    \n" + + " 8.2     “Pseudonymized Data” means data that cannot be used to identify you or\n" + + " any other person without the use of additional information that is kept separately and is subject to\n" + + " technical and organizational measures to ensure that the personal data is not attributed to you or\n" + + " any other person.\n" + + "

    \n" + + " 8.3     You acknowledge and agree that Zebra may, as permitted by law:\n" + + "

    \n" + + " 8.3.1        collect Pseudonymized Data associated with your use\n" + + " of the Software, including data generated by the Software and/or data generated by any device\n" + + " incorporating software that interfaces with the Software;\n" + + "

    \n" + + " 8.3.2        create aggregated data records using the\n" + + " Pseudonymized Data;\n" + + "

    \n" + + " 8.3.3        use the aggregated data records to improve the\n" + + " Software, develop new software or services, understand industry trends, create and publish white\n" + + " papers, reports, or databases summarizing the foregoing, investigate and help address and/or prevent\n" + + " actual or potential unlawful activity, and generally for any legitimate purpose related to Zebra’s\n" + + " business; and\n" + + "

    \n" + + " 8.3.4        retain Pseudonymized Data as Anonymized Data when\n" + + " you delete the Software.\n" + + "

    \n" + + " 8.4     “Machine Data” means usage or status information collected by the\n" + + " Software or hardware that interfaces with the Software, such as information related to a computing\n" + + " device running the Software. Example machine data includes remaining usage time, network information\n" + + " (e.g., name or identifier), wireless signal strength, device identifier, software version, hardware\n" + + " version, device type, metadata associated with the operation of the Software, LED state, reboot\n" + + " cause, storage and memory availability or usage, power cycle count, and device up time.\n" + + "

    \n" + + " 8.5     The Software may provide Machine Data to Zebra.\n" + + "

    \n" + + " 8.6     All title and ownership rights in and to Machine Data are held by Zebra.\n" + + " In the event, and to the extent you are deemed to have any ownership rights in Machine Data, you\n" + + " hereby grant Zebra a perpetual, irrevocable, fully paid, royalty free, worldwide license to use\n" + + " Machine Data.\n" + + "

    \n" + + " 9          Modifications of this Agreement\n" + + "
    \n" + + " Modification or amendment of this Agreement must be made through written agreement by authorized\n" + + " representatives of each party. Written agreement may be satisfied by Zebra’s offer of a superseding\n" + + " agreement for your use of the Software and your acceptance thereof by clicking a button presented\n" + + " with the superseding agreement or use of the Software and your acceptance thereof by clicking a\n" + + " button presented with the superseding agreement or using the Software after being presented with the\n" + + " superseding agreement.\n" + + "

    \n" + + " 10          Third-Party Content\n" + + "
    \n" + + " 10.1     The Software may include a link to a third-party resource that makes\n" + + " third-party Content or services available for purchase and/or download from the corresponding\n" + + " third-party.\n" + + "

    \n" + + " 10.2     Access to and use of third-party Content or services is subject to\n" + + " terms and conditions provided by the third-party and may be protected by the third-party’s copyright\n" + + " or other intellectual property rights. Nothing in this Agreement is a license, permission, or\n" + + " assignment of any rights in or to third-party Content or services.\n" + + "

    \n" + + " 10.3     Third-party resources linked or made available via the Software are not\n" + + " considered part of the Software and Zebra may disable integrations of third-party Content or\n" + + " compatibility of the Software with third-party Content at Zebra’s discretion.\n" + + "

    \n" + + " 11          DISCLAIMERS OF WARRANTY AND\n" + + " LIMITATIONS OF LIABILITY\n" + + "
    \n" + + " 11.1     THE SOFTWARE IS PROVIDED \"AS IS\" AND ON AN \"AS AVAILABLE\" BASIS. TO THE\n" + + " FULLEST EXTENT POSSIBLE PURSUANT TO APPLICABLE LAW, ZEBRA DISCLAIMS ALL WARRANTIES, EXPRESS,\n" + + " IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF MERCHANTABILITY,\n" + + " SATISFACTORY QUALITY OR WORKMANLIKE EFFORT, FITNESS FOR A PARTICULAR PURPOSE, RELIABILITY OR\n" + + " AVAILABILITY, ACCURACY, LACK OF VIRUSES, NON-INFRINGEMENT OF THIRD-PARTY RIGHTS OR OTHER VIOLATION\n" + + " OF RIGHTS. ZEBRA DOES NOT WARRANT THAT THE OPERATION OR AVAILABILITY OF THE SOFTWARE WILL BE\n" + + " UNINTERRUPTED OR ERROR FREE. NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED BY YOU FROM\n" + + " ZEBRA OR ITS AFFILIATES SHALL BE DEEMED TO ALTER THIS DISCLAIMER OF WARRANTY REGARDING THE SOFTWARE\n" + + " OR TO CREATE ANY WARRANTY OF ANY SORT FROM ZEBRA. SOME JURISDICTIONS DO NOT ALLOW EXCLUSIONS OR\n" + + " LIMITATIONS OF IMPLIED WARRANTIES, SO SOME OF THE EXCLUSIONS OR LIMITATIONS OF THIS SECTION MAY NOT\n" + + " APPLY TO YOU.\n" + + "

    \n" + + " 11.2     CERTAIN THIRD-PARTY CONTENT MAY BE INCORPORATED WITH OR ACCESSIBLE VIA\n" + + " THE SOFTWARE. ZEBRA MAKES NO REPRESENTATIONS WHATSOEVER ABOUT ANY THIRD-PARTY CONTENT. SINCE ZEBRA\n" + + " HAS LIMITED OR NO CONTROL OVER SUCH CONTENT, YOU ACKNOWLEDGE AND AGREE THAT ZEBRA IS NOT RESPONSIBLE\n" + + " FOR SUCH CONTENT. YOU EXPRESSLY ACKNOWLEDGE AND AGREE THAT USE OF THIRD-PARTY CONTENT IS AT YOUR\n" + + " SOLE RISK AND THAT THE ENTIRE RISK OF UNSATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS\n" + + " WITH YOU. YOU AGREE THAT ZEBRA SHALL NOT BE RESPONSIBLE OR LIABLE, DIRECTLY OR INDIRECTLY, FOR ANY\n" + + " DAMAGE OR LOSS, INCLUDING BUT NOT LIMITED TO ANY DAMAGE TO OR LOSS OF DATA, CAUSED OR ALLEGED TO BE\n" + + " CAUSED BY, OR IN CONNECTION WITH, USE OF OR RELIANCE ON ANY THIRD-PARTY CONTENT AVAILABLE ON OR\n" + + " THROUGH THE SOFTWARE. YOU ACKNOWLEDGE AND AGREE THAT THE USE OF ANY THIRD-PARTY CONTENT IS GOVERNED\n" + + " BY THE THIRD-PARTY’S TERMS OF USE, LICENSE AGREEMENT, PRIVACY POLICY, OR OTHER SUCH AGREEMENT AND\n" + + " THAT ANY INFORMATION OR PERSONAL DATA YOU PROVIDE, WHETHER KNOWINGLY OR UNKNOWINGLY, TO THE\n" + + " THIRD-PARTY, WILL BE SUBJECT TO THE THIRD-PARTY’S PRIVACY POLICY, IF SUCH A POLICY EXISTS. ZEBRA\n" + + " DISCLAIMS ANY RESPONSIBILITY FOR ANY DISCLOSURE OF INFORMATION OR ANY OTHER PRACTICES OF ANY\n" + + " THIRD-PARTY. ZEBRA EXPRESSLY DISCLAIMS ANY WARRANTY REGARDING WHETHER YOUR PERSONAL INFORMATION IS\n" + + " CAPTURED BY ANY THIRD-PARTY OR THE USE TO WHICH SUCH PERSONAL INFORMATION MAY BE PUT BY SUCH\n" + + " THIRD-PARTY.\n" + + "

    \n" + + " 11.3     IN NO EVENT WILL ZEBRA BE LIABLE TO YOU OR ANY OTHER THIRD-PARTY FOR\n" + + " ANY DAMAGES OF ANY KIND ARISING OUT OF OR RELATING TO THE USE OF OR ACCESS TO ANY COMPONENT OF THE\n" + + " SOFTWARE OR THE INABILITY TO USE OR ACCESS ANY COMPONENT OF THE SOFTWARE, INCLUDING BUT NOT LIMITED\n" + + " TO DAMAGES CAUSED BY OR RELATED TO ERRORS, OMISSIONS, INTERRUPTIONS, DEFECTS, DELAY IN OPERATION OR\n" + + " TRANSMISSION, COMPUTER VIRUS, FAILURE TO CONNECT, NETWORK CHARGES, IN-APP PURCHASES, AND ALL OTHER\n" + + " DIRECT, INDIRECT, SPECIAL, INCIDENTAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES EVEN IF ZEBRA HAS BEEN\n" + + " ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR\n" + + " LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THE ABOVE EXCLUSIONS OR LIMITATIONS MAY NOT\n" + + " APPLY TO YOU. NOTWITHSTANDING THE FOREGOING, ZEBRA’S TOTAL LIABILITY TO YOU FOR ALL LOSSES, DAMAGES,\n" + + " CAUSES OF ACTION, INCLUDING BUT NOT LIMITED TO THOSE BASED ON CONTRACT, TORT, OR OTHERWISE, ARISING\n" + + " OUT OF YOUR USE OF THE SOFTWARE OR ANY OTHER PROVISION UNDER THIS AGREEMENT, SHALL NOT EXCEED THE\n" + + " FAIR MARKET VALUE OF THAT COMPONENT OF THE SOFTWARE.\n" + + "

    \n" + + " 11.4     THE FOREGOING LIMITATIONS, EXCLUSIONS, AND DISCLAIMERS HEREIN SHALL\n" + + " APPLY TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, EVEN IF ANY REMEDY FAILS ITS ESSENTIAL\n" + + " PURPOSE. \n" + + "

    \n" + + " 11.5     THE SOFTWARE MAY ENABLE COLLECTION OF LOCATION-BASED DATA FROM ONE OR\n" + + " MORE DEVICES WHICH MAY ALLOW TRACKING OF THE LOCATION OF THOSE DEVICES. ZEBRA SPECIFICALLY DISCLAIMS\n" + + " ANY LIABILITY FOR YOUR USE OR MISUSE OF THE LOCATION-BASED DATA. YOU AGREE TO PAY ALL REASONABLE\n" + + " COSTS AND EXPENSES OF ZEBRA ARISING FROM OR RELATED TO THIRD-PARTY CLAIMS RESULTING FROM YOUR USE OR\n" + + " MISUSE OF THE LOCATION-BASED DATA.\n" + + "

    \n" + + " 12          Third Party Claims\n" + + "
    \n" + + " In the event of a third-party claim against Zebra alleging that your (i) Content, (ii) Feedback, or\n" + + " (iii) Input infringes or misappropriates a third party’s intellectual property rights, you will\n" + + " defend and hold Zebra harmless against such a claim, provided Zebra gives you sufficient notice to\n" + + " fulfill your obligations of this Section 12 without prejudice due to Zebra’s delay.\n" + + "

    \n" + + " 13          Governing Law\n" + + "
    \n" + + " This Agreement is governed by the laws of the State of Illinois, without regard to its conflict of\n" + + " law provisions. This Agreement shall not be governed by the UN Convention on Contracts for the\n" + + " International Sale of Goods, the application of which is expressly excluded. You hereby submit\n" + + " yourself and your property in any legal action or proceeding relating to this Agreement or for\n" + + " recognition and enforcement of any judgment in respect thereof to the exclusive general jurisdiction\n" + + " of the courts of the State of Illinois or to the United States North District Court of Illinois and\n" + + " to the respective appellate courts thereof in connection with any appeal therefrom.\n" + + "

    \n" + + " 14          Handling of Disputes\n" + + "
    \n" + + " 14.1     You acknowledge that, in the event you breach any provision of this\n" + + " Agreement, Zebra may not have an adequate remedy in money or damages. Zebra shall therefore be\n" + + " entitled to seek an injunction against such breach from any court of competent jurisdiction\n" + + " immediately upon request without posting bond. Zebra's right to seek injunctive relief shall not\n" + + " limit its right to seek further remedies.\n" + + "

    \n" + + " 14.2     If any term of this Agreement is to any extent illegal, otherwise\n" + + " invalid, or incapable of being enforced, such term shall be excluded to the extent of such\n" + + " invalidity or unenforceability, all other terms hereof shall remain in full force and effect, and,\n" + + " to the extent permitted and possible, the invalid or unenforceable term shall be deemed replaced by\n" + + " a term that is valid and enforceable and that comes closest to expressing the intention of such\n" + + " invalid or unenforceable term.\n" + + "

    \n" + + " 14.3     You acknowledge that you and Zebra are the sole parties to this\n" + + " Agreement, and you agree to not seek remedies under this Agreement against Zebra’s authorized\n" + + " distributors or resellers with respect to the Software.\n" + + "

    \n" + + " 15          Open Source Software\n" + + "
    \n" + + " The Software may be subject to one or more open source licenses. The open source license provisions\n" + + " may override some terms of this Agreement. Zebra makes the applicable open source licenses available\n" + + " on a legal notices readme file and/or in system reference guides or in command line interface (CLI)\n" + + " reference guides associated with certain Zebra products.\n" + + "

    \n" + + " 16          U.S. Government End User Restricted\n" + + " Rights\n" + + "
    \n" + + " This provision only applies to U.S. Government end users. The Software is a “commercial item” as\n" + + " that term is defined at 48 C.F.R. Part 2.101, consisting of “commercial computer software” and\n" + + " “computer software documentation” as such terms are defined in 48 C.F.R. Part 252.227-7014(a)(1) and\n" + + " 48 C.F.R. Part 252.227-7014(a)(5), and used in 48 C.F.R. Part 12.212 and 48 C.F.R. Part 227.7202, as\n" + + " applicable. Consistent with 48 C.F.R. Part 12.212, 48 C.F.R. Part 252.227-7015, 48 C.F.R. Part\n" + + " 227.7202-1 through 227.7202-4, 48 C.F.R. Part 52.227-19, and other relevant sections of the Code of\n" + + " Federal Regulations, as applicable, the Software is distributed and licensed to U.S. Government end\n" + + " users (a) only as a commercial item, and (b) with only those rights as are granted to all other end\n" + + " users pursuant to the terms and conditions contained herein." + Text( + text = AnnotatedString.fromHtml(eULAHtmlText), + style = TextStyle( + fontSize = 12.sp, + lineHeight = 16.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(400), + color = Color(0xFF636363), + letterSpacing = 0.4.sp, + ) + ) + } + }, + containerColor = Variables.surfaceDefault, + confirmButton = { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Button( + onClick = { showDialog = false }, + modifier = Modifier + .height(40.dp) + .fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = Variables.mainPrimary + ), + shape = RoundedCornerShape(4.dp) + ) + { + Text( + text = "Close", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.stateDefaultEnabled, + textAlign = TextAlign.Center, + ) + ) + } + } + }, + modifier = Modifier.padding(innerPadding) + ) + } + } + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeModelSettings.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeModelSettings.kt new file mode 100644 index 0000000..3aeb02e --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeModelSettings.kt @@ -0,0 +1,225 @@ +package com.zebra.aidatacapturedemo.ui.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.BarcodeSymbology +import com.zebra.aidatacapturedemo.data.FeedbackSettings +import com.zebra.aidatacapturedemo.data.UsecaseState +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +/** + * This file contains the composable functions related to the Barcode Symbology and + * Feedback settings in the AI Data Capture Demo app. It defines the UI components for displaying + * and updating the barcode symbology options and feedback settings based on the current use case + * selected by the user. The functions utilize Jetpack Compose to create a responsive and + * interactive settings screen for the barcode model configuration. + */ +@Composable +fun ExpandableSettingsItemsList.AddBarcodeSettings() { + itemsTitle.add(ExpandableSettingsItem(stringResource(R.string.barcode_symbology))) +} +@Composable +fun ExpandableSettingsItemsList.AddFeedbackSettings() { + itemsTitle.add(ExpandableSettingsItem(stringResource(R.string.feedback))) +} +@Composable +fun AddBarcodeSymbologySwitchOption(viewModel: AIDataCaptureDemoViewModel){ + val currentUIState = viewModel.uiState.collectAsState().value + Column( + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top), + horizontalAlignment = Alignment.Start, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + var currentSymbology = BarcodeSymbology() + if(currentUIState.usecaseSelected == UsecaseState.OCRBarcodeFind.value){ + currentSymbology = currentUIState.ocrBarcodeFindSettings.barcodeSymbology + } else { + currentSymbology = currentUIState.barcodeSettings.barcodeSymbology + } + + SwitchOption(currentSymbology.australian_postal, SwitchOptionData(R.string.australian_postal, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.aztec, SwitchOptionData(R.string.aztec, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.canadian_postal, SwitchOptionData(R.string.canadian_postal, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.chinese_2of5, SwitchOptionData(R.string.chinese_2of5, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.codabar, SwitchOptionData(R.string.codabar, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.code11, SwitchOptionData(R.string.code11, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.code39, SwitchOptionData(R.string.code39, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.code93, SwitchOptionData(R.string.code93, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.code128, SwitchOptionData(R.string.code128, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.composite_ab, SwitchOptionData(R.string.composite_ab, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.composite_c, SwitchOptionData(R.string.composite_c, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.d2of5, SwitchOptionData(R.string.d2of5, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.datamatrix, SwitchOptionData(R.string.datamatrix, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.dotcode, SwitchOptionData(R.string.dotcode, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.dutch_postal, SwitchOptionData(R.string.dutch_postal, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.ean_8, SwitchOptionData(R.string.ean_8, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.ean_13, SwitchOptionData(R.string.ean_13, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.finnish_postal_4s, SwitchOptionData(R.string.finnish_postal_4s, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.grid_matrix, SwitchOptionData(R.string.grid_matrix, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.gs1_databar, SwitchOptionData(R.string.gs1_databar, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.gs1_databar_expanded, SwitchOptionData(R.string.gs1_databar_expanded, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.gs1_databar_lim, SwitchOptionData(R.string.gs1_databar_lim, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.gs1_datamatrix, SwitchOptionData(R.string.gs1_datamatrix, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.gs1_qrcode, SwitchOptionData(R.string.gs1_qrcode, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.hanxin, SwitchOptionData(R.string.hanxin, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.i2of5, SwitchOptionData(R.string.i2of5, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.japanese_postal, SwitchOptionData(R.string.japanese_postal, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.korean_3of5, SwitchOptionData(R.string.korean_3of5, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.mailmark, SwitchOptionData(R.string.mailmark, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.matrix_2of5, SwitchOptionData(R.string.matrix_2of5, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.maxicode, SwitchOptionData(R.string.maxicode, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.micropdf, SwitchOptionData(R.string.micropdf, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.microqr, SwitchOptionData(R.string.microqr, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.msi, SwitchOptionData(R.string.msi, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.pdf417, SwitchOptionData(R.string.pdf417, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.qrcode, SwitchOptionData(R.string.qrcode, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.tlc39, SwitchOptionData(R.string.tlc39, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.trioptic39, SwitchOptionData(R.string.trioptic39, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.uk_postal, SwitchOptionData(R.string.uk_postal, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.upc_a, SwitchOptionData(R.string.upc_a, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.upce0, SwitchOptionData(R.string.upce0, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.upce1, SwitchOptionData(R.string.upce1, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.usplanet, SwitchOptionData(R.string.usplanet, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.uspostnet, SwitchOptionData(R.string.uspostnet, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.us4state, SwitchOptionData(R.string.us4state, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + SwitchOption(currentSymbology.us4state_fics, SwitchOptionData(R.string.us4state_fics, onItemSelected = { title, enabled -> + viewModel.updateBarcodeSymbology(title, enabled) + })) + } +} + +@Composable +fun AddFeedbackSwitchOption(viewModel: AIDataCaptureDemoViewModel) { + val currentUIState = viewModel.uiState.collectAsState().value + Column( + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top), + horizontalAlignment = Alignment.Start, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + var currentFeedback = FeedbackSettings() + if (currentUIState.usecaseSelected == UsecaseState.OCRBarcodeFind.value) { + currentFeedback = currentUIState.ocrBarcodeFindSettings.feedbackSettings + } + SwitchOptionWithTextDescription( + currentFeedback.audioBeep, + SwitchOptionData(R.string.audio, onItemSelected = { title, enabled -> + viewModel.updateFeedback(title, enabled) + }), stringResource(R.string.audio_desc) + ) + SwitchOptionWithTextDescription( + currentFeedback.vibration, + SwitchOptionData(R.string.haptic, onItemSelected = { title, enabled -> + viewModel.updateFeedback(title, enabled) + }), stringResource(R.string.haptic_desc) + ) + SwitchOptionWithTextDescription( + currentFeedback.showDetectedBarcode, + SwitchOptionData(R.string.show_all_detected_barcodes, onItemSelected = { title, enabled -> + viewModel.updateFeedback(title, enabled) + }), stringResource(R.string.show_all_detected_barcodes_desc) + ) + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BulletHandler.kt b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BulletHandler.kt new file mode 100644 index 0000000..ff3c121 --- /dev/null +++ b/AISuite_Demos/Project 2/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BulletHandler.kt @@ -0,0 +1,32 @@ +package com.zebra.aidatacapturedemo.ui.view + +import android.text.Editable +import android.text.Html +import android.text.Spannable +import android.text.Spanned +import android.text.style.BulletSpan +import org.xml.sax.XMLReader + +/** + * Custom Html.TagHandler to handle