diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..92188f9 Binary files /dev/null and b/.DS_Store differ diff --git a/AISuite_Demos/.DS_Store b/AISuite_Demos/.DS_Store new file mode 100644 index 0000000..9cef725 Binary files /dev/null and b/AISuite_Demos/.DS_Store differ 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 7b40c01..049ba32 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" @@ -21,6 +16,7 @@ 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"), @@ -191,6 +187,17 @@ data class AIDataCaptureDemoUiState( var productResults: MutableList = mutableListOf(), val ocrResults: List = listOf(), var barcodeResults: List = listOf(), + var barcodeLabels: Map = emptyMap(), + var pickingBarcodeResults: List = listOf(), + var pickingBarcodeLabels: Map = emptyMap(), + 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 + var pickedProductBarcodes: Set = emptySet(), + var validatedTotes: Set = emptySet(), // Tote Labels confirmed by scan // Choices var isBarcodeModelEnabled: Boolean = true, @@ -201,6 +208,8 @@ data class AIDataCaptureDemoUiState( var ocrBarcodeCaptureSessionCount : Int = 0, var ocrBarcodeCaptureSessionIndex : Int = 0, + var extractedExpirationDate: String? = null, + var selectedFilterType: FilterType = FilterType.NONE, var ocrFilterData: OcrFilterData = FileUtils.loadOcrFilterData(), var barcodeFilterData: BarcodeFilterData = FileUtils.loadBarcodeFilterData() 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..424d343 --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/data/CustomerInfo.kt @@ -0,0 +1,47 @@ +// 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("Purell Hand Sanitizer", 5.99, "073852401097"), + ProductInfo("Maltese", 2.49, "6936749026602"), + ProductInfo("Clip", 3.95, "6936590040130"), + ProductInfo("Premium Skincare Facial Tissue", 7.49, "627987553635"), + ProductInfo("Blue Pen", 1.49, "4901681143122"), + ProductInfo("Red Pen", 1.49, "4901681143139"), + ProductInfo("Green Highlighter", 1.79, "045888781405"), + ProductInfo("Yellow Highlighter", 1.79, "045888783508"), + ProductInfo("Staedtler Mars Plastic Erazer", 2.25, "031901907983"), + ProductInfo("Uni-Ball Black Pen", 1.99, "4902778497814"), + ProductInfo("Ain Stein 0.5 HB", 1.55, "4902506269249"), + ProductInfo("Black board", 1.79, "024680135791"), + ProductInfo("Pencil", 1.25, "4007817182260"), + ProductInfo("Equate Instant Hand Sanitizer Gel", 1.25, "628915089622"), + ProductInfo("Math Sudoku", 1.25, "4952583053415"), + ) + + fun generateCustomers(toteIds: List = listOf("A", "B", "C", "D", "E", "F")): List { + return toteIds.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/model/ExpirationDateParser.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/model/ExpirationDateParser.kt new file mode 100644 index 0000000..2948b0a --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/model/ExpirationDateParser.kt @@ -0,0 +1,109 @@ +package com.zebra.aidatacapturedemo.model + +import com.zebra.aidatacapturedemo.data.ResultData +import java.util.regex.Pattern + +/** + * Utility class to extract expiration dates from OCR text results based on product label analysis rules. + */ +object ExpirationDateParser { + + private val KEYWORDS = listOf( + "EXP", "EXPIRY", "EXPIRES", "EXPIRATION DATE", + "BEST BEFORE", "BEST BY", "BB", + "MA", "MFG" + ) + + private val EXP_KEYWORDS = listOf("EXP", "EXPIRY", "EXPIRES", "EXPIRATION DATE") + + // Regex for various date formats as requested: + // 1. MM/YY or MM/YYYY (e.g. 12/26, 12/2026) + // 2. MM-YY or MM-YYYY (e.g. 12-26, 12-2026) + // 3. MON YYYY (e.g. JAN 2026) + // 4. YYYY-MM-DD (e.g. 2026-12-31) + private const val DATE_PATTERN_STR = + """(\b\d{1,2}[/-]\d{2,4}\b|\b(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)\s+\d{4}\b|\b\d{4}-\d{2}-\d{2}\b)""" + + private val KEYWORD_PATTERN_STR = KEYWORDS.joinToString("|") { Pattern.quote(it) } + + /** + * Extracts the expiration date from the given OCR text. + * + * @param ocrText The full text obtained from OCR. + * @return The extracted date string, or "Not found" if no matching date is found. + */ + fun extractExpirationDate(ocrText: String): String { + if (ocrText.isBlank()) return "Not found" + + // Pattern to find keyword followed by optional separator (: or space) and then the date + val regex = Regex("(?i)($KEYWORD_PATTERN_STR)[:\\s]*$DATE_PATTERN_STR", RegexOption.IGNORE_CASE) + + val matches = regex.findAll(ocrText) + val results = mutableListOf>() + + for (match in matches) { + val keyword = match.groups[1]?.value?.uppercase() ?: "" + val date = match.groups[2]?.value ?: "" + if (date.isNotEmpty()) { + results.add(keyword to date) + } + } + + if (results.isEmpty()) return "Not found" + + // "If multiple dates are found, return the one labeled as expiration specifically" + val expResult = results.find { (kw, _) -> + EXP_KEYWORDS.any { kw.contains(it) } + } + + return expResult?.second ?: results.first().second + } + + /** + * Extracts expiration date from a list of OCR results, handling cases where the keyword + * and date might be in separate ResultData items. + * + * @param results List of ResultData objects from OCR analysis. + * @return The extracted date string, or "Not found". + */ + fun extractFromResults(results: List): String { + if (results.isEmpty()) return "Not found" + + val foundDates = mutableListOf>() + val keywordRegex = Regex("(?i)($KEYWORD_PATTERN_STR)", RegexOption.IGNORE_CASE) + val dateRegex = Regex(DATE_PATTERN_STR, RegexOption.IGNORE_CASE) + val combinedRegex = Regex("(?i)($KEYWORD_PATTERN_STR)[:\\s]*$DATE_PATTERN_STR", RegexOption.IGNORE_CASE) + + for (i in results.indices) { + val text = results[i].text.trim() + + // Case 1: Keyword and Date in the same result item + val match = combinedRegex.find(text) + if (match != null) { + foundDates.add(match.groups[1]!!.value.uppercase() to match.groups[2]!!.value) + continue + } + + // Case 2: Keyword in this result, Date in the next result (sequential) + val keywordMatch = keywordRegex.find(text) + if (keywordMatch != null && i + 1 < results.size) { + val nextText = results[i+1].text.trim() + val dateMatch = dateRegex.find(nextText) + if (dateMatch != null) { + foundDates.add(keywordMatch.value.uppercase() to dateMatch.value) + } + } + } + + if (foundDates.isEmpty()) { + // Last resort: search in the joined string + return extractExpirationDate(results.joinToString(" ") { it.text }) + } + + val expResult = foundDates.find { (kw, _) -> + EXP_KEYWORDS.any { kw.contains(it) } + } + + return expResult?.second ?: foundDates.first().second + } +} diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/model/FileUtils.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/model/FileUtils.kt index cea418f..28912a5 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/model/FileUtils.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/model/FileUtils.kt @@ -26,6 +26,7 @@ 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 @@ -243,6 +244,20 @@ class FileUtils(cacheDir: String, context : Context) { } } + 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")) } diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureDemoApp.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureDemoApp.kt index 135c279..d8c1118 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureDemoApp.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureDemoApp.kt @@ -337,29 +337,32 @@ fun AIDataCaptureDemoAppBarTitle( viewModel: AIDataCaptureDemoViewModel, uiState: AIDataCaptureDemoUiState ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), - verticalAlignment = Alignment.CenterVertically, + Box( 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, + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically, 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, + .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, + ) ) - ) + } } } 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/BarcodeMapPickingScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt new file mode 100644 index 0000000..10ec5e8 --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt @@ -0,0 +1,377 @@ +// 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.content.BroadcastReceiver +import android.content.Intent +import android.content.IntentFilter +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 +import android.annotation.SuppressLint + +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.viewinterop.AndroidView +import androidx.camera.view.PreviewView +import androidx.compose.ui.platform.LocalLifecycleOwner +import android.util.Size + +@SuppressLint("UnusedContentLambdaTargetStateParameter") +@Composable +fun BarcodeMapPickingScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + @Suppress("UNUSED_PARAMETER") context: Context, + @Suppress("UNUSED_PARAMETER") activityInnerPadding: PaddingValues, + innerPadding: PaddingValues, + activityLifecycle: Lifecycle +) { + val uiState by viewModel.uiState.collectAsState() + val localContext = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + viewModel.updateAppBarTitle("Picking Map") + + // Initialize Camera for Live Scanning on Map + LaunchedEffect(Unit) { + // We use a standard resolution for picking + viewModel.updateCameraReady(false) + } + + // 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) + } + } + } + localContext.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED) + onDispose { + localContext.unregisterReceiver(receiver) + } + } + + // Removed automatic selectedToteId update to prevent overwriting "Show on Map" target + /* + LaunchedEffect(uiState.barcodeResults) { + if (uiState.barcodeResults.isNotEmpty()) { + val detectedText = uiState.barcodeResults.first().text + viewModel.updateSelectedToteId(detectedText) + } + } + */ + + Box(modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + // 1. Background Camera Preview (Low alpha to ensure it's active but hidden) + AndroidView( + factory = { ctx -> + PreviewView(ctx).apply { + this.scaleType = PreviewView.ScaleType.FILL_CENTER + viewModel.setupCameraController( + previewView = this, + analysisUseCaseCameraResolution = Size(1280, 720), + lifecycleOwner = lifecycleOwner, + activityLifecycle = activityLifecycle + ) + } + }, + modifier = Modifier.fillMaxSize().alpha(0.1f) + ) + + // 2. Full screen Abstract Map (The "Digital Twin") + AbstractMapLayer(uiState) + + // 3. Guidance Overlay + val feedback = uiState.pickingFeedback + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 100.dp), // Increased padding to avoid being blocked by top bar + contentAlignment = Alignment.TopCenter + ) { + if (feedback != null) { + val isWarning = feedback.contains("incorrect", ignoreCase = true) || + feedback.contains("already picked", ignoreCase = true) || + feedback.contains("Unrecognized", ignoreCase = true) + + Text( + text = feedback, + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ), + modifier = Modifier + .background( + if (isWarning) Color.Red else Color(0xFF006D39), + RoundedCornerShape(8.dp) + ) + .padding(horizontal = 24.dp, vertical = 12.dp) + ) + } else { + Text( + text = "Scan Tote Barcode", + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ), + modifier = Modifier + .background(Color.White.copy(alpha = 0.9f), RoundedCornerShape(8.dp)) + .padding(horizontal = 24.dp, vertical = 12.dp) + ) + } + } + } +} + +@Composable +private fun AbstractMapLayer(uiState: AIDataCaptureDemoUiState) { + val barcodeResults = uiState.pickingBarcodeResults + if (barcodeResults.isEmpty()) return + + // Calculate the bounding box of all detected barcodes to center the content + val minX = barcodeResults.minOf { it.boundingBox.left }.toFloat() + val maxX = barcodeResults.maxOf { it.boundingBox.right }.toFloat() + val minY = barcodeResults.minOf { it.boundingBox.top }.toFloat() + val maxY = barcodeResults.maxOf { it.boundingBox.bottom }.toFloat() + + val contentWidth = maxX - minX + val contentHeight = maxY - minY + + 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() + + // Use 80% of the screen to keep them "far from the side" + val paddingFactor = 0.8f + val availableWidth = displayTotalWidthInPx * paddingFactor + val availableHeight = displayTotalHeightInPx * paddingFactor + + val scaler = min( + availableWidth / contentWidth, + availableHeight / contentHeight + ).coerceAtMost(displayMetricsDensity * 2.0f) // Cap scaler so single barcodes don't explode + + // Calculate offsets to center the content bounding box on screen + val gapX = (displayTotalWidthInPx - (scaler * contentWidth)) / 2f - (scaler * minX) + val gapY = (displayTotalHeightInPx - (scaler * contentHeight)) / 2f - (scaler * minY) + + 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.pickingBarcodeResults + if (barcodeResults.isEmpty()) return + + // Grouping logic for columns first (to identify vertical stacks) + val sortedByX = barcodeResults.sortedBy { it.boundingBox.centerX() } + val columns = mutableListOf>() + + if (sortedByX.isNotEmpty()) { + var currentColumn = mutableListOf() + currentColumn.add(sortedByX[0]) + columns.add(currentColumn) + + for (i in 1 until sortedByX.size) { + val prev = sortedByX[i - 1] + val curr = sortedByX[i] + // Overlap threshold for same column: 60% of width + if (abs(curr.boundingBox.centerX() - prev.boundingBox.centerX()) < (prev.boundingBox.width() * 0.6)) { + currentColumn.add(curr) + } else { + currentColumn = mutableListOf() + currentColumn.add(curr) + columns.add(currentColumn) + } + } + } + + // Sort each column by Y (top-to-bottom) + columns.forEach { it.sortBy { item -> item.boundingBox.centerY() } } + + // Synthesize rows from columns for consistent alignment and labeling + val rows = mutableListOf>() + val maxItemsInColumn = columns.maxOfOrNull { it.size } ?: 0 + for (rowIdx in 0 until maxItemsInColumn) { + val row = mutableListOf() + for (colIdx in 0 until columns.size) { + if (rowIdx < columns[colIdx].size) { + row.add(columns[colIdx][rowIdx]) + } + } + rows.add(row) + } + + 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() + + sortedRow.forEach { barcode -> + val bBoxWidth = barcode.boundingBox.width().toFloat() + val centerX = barcode.boundingBox.centerX().toFloat() + + val scaledWidth = (scaler * bBoxWidth) * 2.0f + val scaledHeight = (scaler * avgHeight) * 3.5f + val scaledLeft = (scaler * centerX) + gapX - (scaledWidth / 2) + val scaledTop = (scaler * avgCenterY) + gapY - (scaledHeight / 2) + + // Use the pre-calculated labels from the ViewModel + val label = uiState.pickingBarcodeLabels[barcode.text] ?: "" + + // Find quantity if this tote is one of the targets for the current product + val qty = uiState.targetTotes.find { it.first == label }?.second + + // Check if this specific tote box has already been validated by a scan + val isValidated = uiState.validatedTotes.contains(label) + + // Highlight if this box's label matches the selected tote OR it's a target AND not validated yet + val isTarget = (uiState.selectedToteId == label || qty != null) && !isValidated + + val displayText = if (qty != null && !isValidated) "QTY: $qty" else barcode.text + + val textColor = Color.Black + + drawAbstractPickingUnit( + barcode = displayText, + label = label, + left = scaledLeft, + top = scaledTop, + width = scaledWidth, + height = scaledHeight, + density = displayMetricsDensity, + isTarget = isTarget + ) + } + } + } +} + +private fun DrawScope.drawAbstractPickingUnit( + barcode: String, + label: String, + left: Float, + top: Float, + width: Float, + height: Float, + density: Float, + isTarget: Boolean +) { + val themeColor = if (isTarget) Color(0xFF72D2FF) 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(0xFF0078EE) else themeColor, + topLeft = topLeft, + size = rectSize, + style = Stroke(width = (if (isTarget) 4f else 2f) * density) + ) + + // Draw label badge above the box + if (label.isNotEmpty()) { + val radius = 12f * density + val centerX = left + width / 2 + val centerY = top - radius - 2f * density // Positioned above the box + + drawCircle( + color = if (isTarget) Color(0xFF0078EE) else Color(0xFF006D39), + radius = radius, + center = Offset(centerX, centerY) + ) + + val labelPaint = android.graphics.Paint().apply { + this.color = android.graphics.Color.WHITE + this.textSize = 10f * density + this.textAlign = android.graphics.Paint.Align.CENTER + this.isAntiAlias = true + this.isFakeBoldText = true + } + + val labelY = centerY - (labelPaint.fontMetrics.ascent + labelPaint.fontMetrics.descent) / 2 + drawContext.canvas.nativeCanvas.drawText(label, centerX, labelY, labelPaint) + } + + val paint = android.graphics.Paint().apply { + this.color = if (isTarget) android.graphics.Color.BLACK else android.graphics.Color.BLACK + this.textSize = (if (isTarget) 14f else 11f) * 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 > 20 * density) { + val displayId = if (barcode.startsWith("QTY:")) barcode else if (barcode.length > 5) barcode.takeLast(5) else barcode + 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 new file mode 100644 index 0000000..13d98c3 --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt @@ -0,0 +1,337 @@ +// 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.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.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 +import androidx.compose.ui.graphics.drawscope.DrawScope +import com.zebra.aidatacapturedemo.data.ResultData +import kotlin.math.abs + +/** + * BarcodeMapResultScreen is a Composable function that displays an abstract + * geometrical layout of detected barcodes 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("Layout 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 + + // Calculate the bounding box of all detected barcodes to center the content + val barcodeResults = uiState.barcodeResults + val minX = if (barcodeResults.isNotEmpty()) barcodeResults.minOf { it.boundingBox.left }.toFloat() else 0f + val maxX = if (barcodeResults.isNotEmpty()) barcodeResults.maxOf { it.boundingBox.right }.toFloat() else 1f + val minY = if (barcodeResults.isNotEmpty()) barcodeResults.minOf { it.boundingBox.top }.toFloat() else 0f + val maxY = if (barcodeResults.isNotEmpty()) barcodeResults.maxOf { it.boundingBox.bottom }.toFloat() else 1f + + val contentWidth = maxX - minX + val contentHeight = maxY - minY + + // Use 80% of the screen to keep them "far from the side" + val paddingFactor = 0.8f + val availableWidth = displayTotalWidthInPx * paddingFactor + val availableHeight = availableHeightInPx * paddingFactor + + val scaler = if (barcodeResults.isNotEmpty()) { + min(availableWidth / contentWidth, availableHeight / contentHeight) + .coerceAtMost(displayMetricsDensity * 2.0f) + } else 1f + + // Calculate offsets to center the content bounding box on screen + val gapX = (displayTotalWidthInPx - (scaler * contentWidth)) / 2f - (scaler * minX) + val gapY = (availableHeightInPx - (scaler * contentHeight)) / 2f - (scaler * minY) + + Box( + modifier = Modifier + .fillMaxSize() + .padding(activityInnerPadding) + .background(color = Color(0xFFF0F2F5)) // Clean modern background + ) { + // ABSTRACT MAP CANVAS + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + if (uiState.barcodeResults.isEmpty()) { + CircularProgressIndicator(color = Color(0xFF006D39)) + } else { + DrawAbstractBarcodeMap( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + } + } + + + // SAVE BUTTON + Box( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 32.dp), + contentAlignment = Alignment.BottomCenter + ) { + Button( + onClick = { + viewModel.saveBarcodeLayout() + navController.navigate(Screen.CustomerInformation.route) + }, + modifier = Modifier + .fillMaxWidth(0.6f) + .height(54.dp), + shape = RoundedCornerShape(27.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF006D39)) // Dark green + ) { + Text( + text = "Save Barcode Map", + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = Color.White + ) + ) + } + } + } + } +} + +@Composable +private fun DrawAbstractBarcodeMap( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float +) { + val barcodeResults = uiState.barcodeResults + if (barcodeResults.isEmpty()) return + + // Grouping logic for columns first (to identify vertical stacks) + val sortedByX = barcodeResults.sortedBy { it.boundingBox.centerX() } + val columns = mutableListOf>() + + if (sortedByX.isNotEmpty()) { + var currentColumn = mutableListOf() + currentColumn.add(sortedByX[0]) + columns.add(currentColumn) + + for (i in 1 until sortedByX.size) { + val prev = sortedByX[i - 1] + val curr = sortedByX[i] + // Overlap threshold for same column: 60% of width + if (abs(curr.boundingBox.centerX() - prev.boundingBox.centerX()) < (prev.boundingBox.width() * 0.6)) { + currentColumn.add(curr) + } else { + currentColumn = mutableListOf() + currentColumn.add(curr) + columns.add(currentColumn) + } + } + } + + // Sort each column by Y (top-to-bottom) + columns.forEach { it.sortBy { item -> item.boundingBox.centerY() } } + + // Synthesize rows from columns for consistent alignment and labeling + val rows = mutableListOf>() + val maxItemsInColumn = columns.maxOfOrNull { it.size } ?: 0 + for (rowIdx in 0 until maxItemsInColumn) { + val row = mutableListOf() + for (colIdx in 0 until columns.size) { + if (rowIdx < columns[colIdx].size) { + row.add(columns[colIdx][rowIdx]) + } + } + rows.add(row) + } + + 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() + + sortedRow.forEachIndexed { index, barcode -> + val bBoxWidth = barcode.boundingBox.width().toFloat() + val centerX = barcode.boundingBox.centerX().toFloat() + + val scaledWidth = (scaler * bBoxWidth) * 2.0f + val scaledHeight = (scaler * avgHeight) * 3.5f + val scaledLeft = (scaler * centerX) + gapX - (scaledWidth / 2) + val scaledTop = (scaler * avgCenterY) + gapY - (scaledHeight / 2) + + // Use pre-calculated labels + val label = uiState.barcodeLabels[barcode.text] ?: "" + + drawAbstractUnit( + barcode = barcode.text, + label = label, + left = scaledLeft, + top = scaledTop, + width = scaledWidth, + height = scaledHeight, + density = displayMetricsDensity + ) + } + } + } +} + +private fun DrawScope.drawAbstractUnit( + barcode: String, + label: 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) + + // 1. Draw simple geometrical shape (Rectangle) + drawRect( + color = themeColor.copy(alpha = 0.2f), + topLeft = topLeft, + size = rectSize + ) + + // 2. Draw sharp outline + drawRect( + color = themeColor, + topLeft = topLeft, + size = rectSize, + style = Stroke(width = 2f * density) + ) + + // 3. Draw label badge above the box + if (label.isNotEmpty()) { + val radius = 12f * density + val centerX = left + width / 2 + val centerY = top - radius - 2f * density // Positioned above the box + + drawCircle( + color = Color(0xFF006D39), + radius = radius, + center = Offset(centerX, centerY) + ) + + val labelPaint = android.graphics.Paint().apply { + this.color = android.graphics.Color.WHITE + this.textSize = 10f * density + this.textAlign = android.graphics.Paint.Align.CENTER + this.isAntiAlias = true + this.isFakeBoldText = true + } + + val labelY = centerY - (labelPaint.fontMetrics.ascent + labelPaint.fontMetrics.descent) / 2 + drawContext.canvas.nativeCanvas.drawText(label, centerX, labelY, labelPaint) + } + + // 4. Center-aligned Barcode text + val paint = android.graphics.Paint().apply { + this.color = android.graphics.Color.BLACK + this.textSize = 11f * 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 + + // Only draw ID if it fits within the simplified shape + if (width > 20 * density) { + val displayId = if (barcode.length > 5) barcode.takeLast(5) else barcode + drawContext.canvas.nativeCanvas.drawText(displayId, textX, textY, paint) + } +} diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapScanPickingScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapScanPickingScreen.kt new file mode 100644 index 0000000..698d318 --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapScanPickingScreen.kt @@ -0,0 +1,163 @@ +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.viewmodel.AIDataCaptureDemoViewModel + +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll + +@Composable +fun BarcodeMapScanPickingScreen( + 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("") } + val scrollState = rememberScrollState() + + viewModel.updateAppBarTitle("Product Scan") + + // 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)) + .verticalScroll(scrollState) + .padding(innerPadding) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // ... (rest of the text field and button) + 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) { + val isError = feedback.contains("Incorrect") || feedback.contains("already picked") + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = if (isError) 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 (isError) Color.Red else Color(0xFF2E7D32) + ) + ) + + val product = uiState.lastScannedProduct + if (product != null && !isError) { + Spacer(modifier = Modifier.height(16.dp)) + Text(text = "Product: ${product.name}", color = Color.Black, fontWeight = FontWeight.Bold) + + uiState.targetTotes.forEach { pair -> + val toteId = pair.first + val qty = pair.second + val label = uiState.barcodeLabels[toteId] + val displayText = if (label != null) "Tote $label ($toteId)" else "Tote $toteId" + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = displayText, 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 c9eb016..ce68fdb 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("Product 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) { @@ -156,7 +163,8 @@ fun CameraPreviewScreen( viewModel.updateOcrResultData(results = null) } - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, + UsecaseState.BarcodeMap.value -> { viewModel.updateBarcodeResultData(results = listOf()) } @@ -333,7 +341,8 @@ fun CameraPreviewScreen( ) } - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, + UsecaseState.BarcodeMap.value -> { DrawBarcodeResult( uiState = uiState, scaler = scaler, @@ -385,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 { @@ -818,6 +852,30 @@ fun DrawBarcodeResult( size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), style = Stroke(width = (1f * displayMetricsDensity)) ) + + // Draw label badge above the box + val label = uiState.barcodeLabels[barcodeData.text] + if (label != null) { + val radius = 10f * displayMetricsDensity + val centerX = scaledBBoxLeftInPx + rectangleWidth / 2 + val centerY = scaledBBoxTopInPx - radius - 2f * displayMetricsDensity + + drawCircle( + color = Color(0xFF006D39), + radius = radius, + center = Offset(centerX, centerY) + ) + + val labelPaint = android.graphics.Paint().apply { + this.color = android.graphics.Color.WHITE + this.textSize = 8f * displayMetricsDensity + this.textAlign = android.graphics.Paint.Align.CENTER + this.isAntiAlias = true + this.isFakeBoldText = true + } + val labelY = centerY - (labelPaint.fontMetrics.ascent + labelPaint.fontMetrics.descent) / 2 + drawContext.canvas.nativeCanvas.drawText(label, centerX, labelY, labelPaint) + } } } } @@ -834,7 +892,7 @@ fun DrawBarcodeResult( if (barcodeData.text != null && barcodeData.text != "") { Text( text = barcodeData.text, - fontSize = 10.sp, + fontSize = 14.sp, color = Color.White, style = TextStyle( platformStyle = PlatformTextStyle( @@ -1163,6 +1221,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 +1248,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/CustomerInformationScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt new file mode 100644 index 0000000..eca1b04 --- /dev/null +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt @@ -0,0 +1,196 @@ +package com.zebra.aidatacapturedemo.ui.view + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.ProductInfo +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +@Composable +fun CustomerInformationScreen( + viewModel: AIDataCaptureDemoViewModel, + navController: NavController, + innerPadding: PaddingValues +) { + val uiState by viewModel.uiState.collectAsState() + viewModel.updateAppBarTitle("Product List") + + val customers = uiState.allCustomers + + // 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]!! } + } + + val toPickGroups = productGroups.mapNotNull { (product, totes) -> + val remainingTotes = totes.filter { (toteId, _) -> + !uiState.pickedProductBarcodes.contains("${product.barcode}:$toteId") + } + if (remainingTotes.isNotEmpty()) product to remainingTotes else null + } + + val pickedGroups = productGroups.mapNotNull { (product, totes) -> + val pickedTotes = totes.filter { (toteId, _) -> + uiState.pickedProductBarcodes.contains("${product.barcode}:$toteId") + } + if (pickedTotes.isNotEmpty()) product to pickedTotes else null + } + + var selectedTabIndex by remember { mutableIntStateOf(0) } + val tabs = listOf("Products to Pick", "Products Already Picked") + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFFF8F9FA)) + .padding(innerPadding) + ) { + TabRow( + selectedTabIndex = selectedTabIndex, + containerColor = Color.White, + contentColor = Color(0xFF006D39), + indicator = { tabPositions -> + TabRowDefaults.Indicator( + Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]), + color = Color(0xFF006D39) + ) + } + ) { + tabs.forEachIndexed { index, title -> + Tab( + selected = selectedTabIndex == index, + onClick = { selectedTabIndex = index }, + text = { + Text( + text = title, + color = if (selectedTabIndex == index) Color(0xFF006D39) else Color.Gray, + fontWeight = if (selectedTabIndex == index) FontWeight.Bold else FontWeight.Normal + ) + } + ) + } + } + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + val currentGroups = if (selectedTabIndex == 0) toPickGroups else pickedGroups + + items(currentGroups) { (product, totes) -> + ProductPickingItem(product, totes) + } + + if (selectedTabIndex == 0) { + 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) + } + } + } + + item { + Spacer(modifier = Modifier.height(48.dp)) + } + } + } +} + +@Composable +fun ProductPickingItem(product: ProductInfo, totes: List>) { + var expanded by remember { mutableStateOf(false) } + + Card( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = !expanded } + .animateContentSize(), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = product.name, + style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold, color = Color.Black) + ) + Text( + text = "Barcode: ${if (product.barcode.length > 5) product.barcode.takeLast(5) else product.barcode}", + style = TextStyle(fontSize = 14.sp, color = Color.Gray) + ) + } + Icon( + imageVector = if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, + contentDescription = if (expanded) "Collapse" else "Expand", + tint = Color.Gray + ) + } + + if (expanded) { + Spacer(modifier = Modifier.height(16.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 = 4.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/DemoSettingsScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt index 5e9a450..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 @@ -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) } @@ -646,6 +646,13 @@ fun AddAboutInformation(viewModel: AIDataCaptureDemoViewModel) { ) } + 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/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..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 @@ -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 } @@ -171,6 +171,10 @@ fun getSettingHeading(demo: String): Int? { settingsString = R.string.barcode_settings } + UsecaseState.BarcodeMap.value -> { + settingsString = R.string.barcode_map_settings + } + UsecaseState.OCR.value -> { settingsString = R.string.text_ocr_recognizer_settings } @@ -197,6 +201,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 +253,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 +304,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..81f2a0a 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,10 @@ 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 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") /** @@ -114,6 +118,42 @@ fun NavigationStack( 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) + BarcodeMapScanPickingScreen( + 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, + innerPadding = innerPadding, + 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/ui/view/OCRBarcodeResultCapturedScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultCapturedScreen.kt index 4756254..d8c4fb6 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultCapturedScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultCapturedScreen.kt @@ -233,8 +233,9 @@ private fun DrawBarcodeResultOnCanvas( scaledBBoxLeftInPx, scaledBBoxTopInPx + (barcodeRectangleHeight) / 2 ) + val displayId = if (barcodeData.text.length > 5) barcodeData.text.takeLast(5) else barcodeData.text drawContext.canvas.nativeCanvas.drawText( - barcodeData.text, + displayId, barcodeTextOffset.x, barcodeTextOffset.y, paint diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultScreen.kt index a549a8a..9b612b6 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultScreen.kt @@ -85,7 +85,8 @@ fun OCRBarcodeResultScreen( } val barcodeList = uiState.barcodeResults.isNullOrEmpty().let { uiState.barcodeResults.filter { it.text.isNotEmpty() }.map { - ResultRowData(it.text, it.boundingBox, isBarcode = true) + val displayId = if (it.text.length > 5) it.text.takeLast(5) else it.text + ResultRowData(displayId, it.boundingBox, isBarcode = true) } } resultList.clear() @@ -263,7 +264,8 @@ private fun loadSessionResults(context: Context, uiState: AIDataCaptureDemoUiSta } val barcodeList = if (!sessionJson?.barcodeResults.isNullOrEmpty()) { sessionJson.barcodeResults.filter { it.text.isNotEmpty() }.map { - ResultRowData(it.text, it.boundingBox, isBarcode = true) + val displayId = if (it.text.length > 5) it.text.takeLast(5) else it.text + ResultRowData(displayId, it.boundingBox, isBarcode = true) } } else { emptyList() diff --git a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/CharacterMatchFilterScreen.kt b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/CharacterMatchFilterScreen.kt index 21c8ce5..42cb771 100644 --- a/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/CharacterMatchFilterScreen.kt +++ b/AISuite_Demos/AIDataCaptureDemo/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/CharacterMatchFilterScreen.kt @@ -442,7 +442,7 @@ fun CharacterMatchFilterScreen( // Save Button Button( onClick = { - viewModel.updateToastMessage("Save was successful.") + viewModel.updateToastMessage("Save was successfully.") if (uiState.selectedFilterType == FilterType.OCR_FILTER) { val defaultOcrFilterData = uiState.ocrFilterData 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..9acdb15 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 @@ -86,6 +88,7 @@ import java.io.IOException import java.io.InputStreamReader import java.util.concurrent.Executor import kotlin.coroutines.resume +import kotlin.math.abs import kotlin.coroutines.resumeWithException private const val TAG = "AIDataCaptureDemoViewModel" @@ -166,6 +169,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 +233,7 @@ class AIDataCaptureDemoViewModel( */ fun deinitModel() { when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { barcodeAnalyzer?.deinitialize() barcodeAnalyzer = null } @@ -331,6 +347,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 +582,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 +614,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 +649,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 +686,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 +705,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 +737,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 +756,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 +785,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 +814,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 +1641,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 +1669,7 @@ class AIDataCaptureDemoViewModel( fun restoreDefaultSettings() { when (_uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { _uiState.value.barcodeSettings = BarcodeSettings() } @@ -1768,11 +1785,165 @@ class AIDataCaptureDemoViewModel( } fun updateBarcodeResultData(results: List) { + val labels = calculateBarcodeLabels(results) _uiState.update { it -> it.copy( - barcodeResults = results + barcodeResults = results, + barcodeLabels = labels ) } + + // Handle Picking Logic if we are in picking flow + if (uiState.value.selectedCustomer != null && results.isNotEmpty()) { + handlePickingScan(results) + } + } + + private fun calculateBarcodeLabels(results: List): Map { + if (results.isEmpty()) return emptyMap() + + // Grouping logic for columns (left-to-right) to identify vertical stacks + val sortedByX = results.sortedBy { it.boundingBox.centerX() } + val columns = mutableListOf>() + + if (sortedByX.isNotEmpty()) { + var currentColumn = mutableListOf() + currentColumn.add(sortedByX[0]) + columns.add(currentColumn) + + for (i in 1 until sortedByX.size) { + val prev = sortedByX[i - 1] + val curr = sortedByX[i] + // Overlap threshold for same column: 60% of width + if (abs(curr.boundingBox.centerX() - prev.boundingBox.centerX()) < (prev.boundingBox.width() * 0.6)) { + currentColumn.add(curr) + } else { + currentColumn = mutableListOf() + currentColumn.add(curr) + columns.add(currentColumn) + } + } + } + + // Sort each column by Y (top-to-bottom) + columns.forEach { it.sortBy { item -> item.boundingBox.centerY() } } + + val labelMap = mutableMapOf() + var labelCounter = 0 + + // Labeling row-by-row across columns (Top item of each column, then second item, etc.) + val maxItemsInColumn = columns.maxOfOrNull { it.size } ?: 0 + for (rowIdx in 0 until maxItemsInColumn) { + for (colIdx in 0 until columns.size) { + if (rowIdx < columns[colIdx].size) { + val barcode = columns[colIdx][rowIdx] + labelMap[barcode.text] = getLabelFromIndex(labelCounter++) + } + } + } + return labelMap + } + + private fun getLabelFromIndex(index: Int): String { + var n = index + val sb = StringBuilder() + do { + sb.append(('A' + (n % 26))) + n = n / 26 - 1 + } while (n >= 0) + return sb.reverse().toString() + } + + private fun handlePickingScan(results: List) { + if (results.isEmpty()) return + val barcode = results.first().text + processHardwareScan(barcode) + } + + private fun processScanResult(barcode: String) { + val currentState = uiState.value + val customers = currentState.allCustomers + val productMatches = mutableListOf>() + var productFound: ProductInfo? = null + + // 1. Check if it's a Product Scan + customers.forEach { customer -> + customer.products.find { it.barcode == barcode }?.let { product -> + // Check if this product has already been picked for this customer/tote + if (!currentState.pickedProductBarcodes.contains("${product.barcode}:${customer.id}")) { + productMatches.add(customer.id to product.quantity) + productFound = product + } + } + } + + if (productMatches.isNotEmpty()) { + _uiState.update { it.copy( + lastScannedProduct = productFound, + targetTotes = productMatches, + pickingFeedback = "Product Identified: ${productFound?.name}.", + validatedTotes = emptySet() // Clear previous validations for the new product + ) } + return + } + + // 2. Check if it's a Tote Scan + // A tote scan usually happens after a product is identified + if (currentState.lastScannedProduct != null) { + // First check direct barcode match, then label match + var toteLabel = currentState.barcodeLabels[barcode] + // Allow manual entry of tote label (A, B, C...) + if (toteLabel == null && currentState.barcodeLabels.values.contains(barcode)) { + toteLabel = barcode + } + + if (toteLabel != null) { + val matchingTote = currentState.targetTotes.find { it.first == toteLabel } + if (matchingTote != null) { + val updatedTargetTotes = currentState.targetTotes.filter { it.first != toteLabel } + val isLastTote = updatedTargetTotes.isEmpty() + val currentBarcode = currentState.lastScannedProduct!!.barcode + + _uiState.update { it.copy( + pickingFeedback = if (isLastTote) + "Correct Tote! All placements for ${currentState.lastScannedProduct!!.name} completed." + else "Correct Tote! Item placed in Tote $toteLabel.", + pickedProductBarcodes = it.pickedProductBarcodes + "$currentBarcode:$toteLabel", + lastScannedProduct = if (isLastTote) null else it.lastScannedProduct, + targetTotes = updatedTargetTotes, + validatedTotes = it.validatedTotes + toteLabel // Helps in map visualization + ) } + return + } else { + _uiState.update { it.copy( + pickingFeedback = "Item placed in wrong tote! Target totes: ${currentState.targetTotes.joinToString { it.first }}" + ) } + toast("Item placed in wrong tote!") + } + return + } + } + + // 3. Fallback: Not a product and not a known tote + _uiState.update { it.copy( + pickingFeedback = "Unrecognized Barcode: $barcode" + ) } + } + + 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) { + processScanResult(barcode) } fun updateRetailShelfDetectionResult(results: Array?) { @@ -1873,11 +2044,41 @@ 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() } + fun saveBarcodeLayout() { + if (uiState.value.barcodeResults.isNotEmpty()) { + FileUtils.saveBarcodeResultsToFile(uiState.value.barcodeResults) + initializePickingDemo() + toast("Barcode layout saved successfully") + } else { + toast("No barcode results to save") + } + } + + private fun initializePickingDemo() { + val toteLabels = uiState.value.barcodeLabels.values.distinct().sorted() + val customers = if (toteLabels.isNotEmpty()) { + com.zebra.aidatacapturedemo.data.CustomerDataGenerator.generateCustomers(toteLabels) + } else { + com.zebra.aidatacapturedemo.data.CustomerDataGenerator.generateCustomers() + } + _uiState.update { it.copy( + allCustomers = customers, + pickingBarcodeResults = it.barcodeResults, + pickingBarcodeLabels = it.barcodeLabels, + pickedProductBarcodes = emptySet(), + pickingFeedback = null, + lastScannedProduct = null, + targetTotes = listOf() + ) } + } + fun toast(toastString: String) { Toast.makeText(context, toastString, Toast.LENGTH_LONG).show() } @@ -1892,7 +2093,7 @@ class AIDataCaptureDemoViewModel( fun stopPreviewAnalysis() { when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { } @@ -1916,7 +2117,7 @@ class AIDataCaptureDemoViewModel( fun startPreviewAnalysis() { when (uiState.value.usecaseSelected) { - UsecaseState.Barcode.value -> { + UsecaseState.Barcode.value, UsecaseState.BarcodeMap.value -> { } @@ -1944,6 +2145,10 @@ class AIDataCaptureDemoViewModel( } + UsecaseState.BarcodeMap.value -> { + barcodeAnalyzer!!.executeHighRes(highResBitmap) + } + UsecaseState.Retail.value -> { } @@ -2000,6 +2205,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 3c6ef82..3d4c85f 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 @@ -38,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_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) { // { diff --git a/AISuite_Demos/Project 2/.gitignore b/AISuite_Demos/Project 2/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/AISuite_Demos/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/README.md b/AISuite_Demos/Project 2/README.md new file mode 100644 index 0000000..9f55c50 --- /dev/null +++ b/AISuite_Demos/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/app/.gitignore b/AISuite_Demos/Project 2/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/AISuite_Demos/Project 2/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/AISuite_Demos/Project 2/app/build.gradle.kts b/AISuite_Demos/Project 2/app/build.gradle.kts new file mode 100644 index 0000000..8792b45 --- /dev/null +++ b/AISuite_Demos/Project 2/app/build.gradle.kts @@ -0,0 +1,134 @@ +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) + testImplementation(libs.junit) +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/app/proguard-rules.pro b/AISuite_Demos/Project 2/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/AISuite_Demos/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/app/src/main/AndroidManifest.xml b/AISuite_Demos/Project 2/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..11cbeae --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/AndroidManifest.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AISuite_Demos/Project 2/app/src/main/assets/barcode_model_input_size.html b/AISuite_Demos/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/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/app/src/main/assets/barcode_resolution.html b/AISuite_Demos/Project 2/app/src/main/assets/barcode_resolution.html new file mode 100644 index 0000000..435f89a --- /dev/null +++ b/AISuite_Demos/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/app/src/main/assets/barcode_symbologies.html b/AISuite_Demos/Project 2/app/src/main/assets/barcode_symbologies.html new file mode 100644 index 0000000..28d6dfa --- /dev/null +++ b/AISuite_Demos/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/app/src/main/assets/ocr_model_input_size.html b/AISuite_Demos/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/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/app/src/main/assets/ocr_resolution.html b/AISuite_Demos/Project 2/app/src/main/assets/ocr_resolution.html new file mode 100644 index 0000000..e35303a --- /dev/null +++ b/AISuite_Demos/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/app/src/main/assets/processor.html b/AISuite_Demos/Project 2/app/src/main/assets/processor.html new file mode 100644 index 0000000..bff9067 --- /dev/null +++ b/AISuite_Demos/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/app/src/main/assets/product_model_input_size.html b/AISuite_Demos/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/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/app/src/main/assets/product_resolution.html b/AISuite_Demos/Project 2/app/src/main/assets/product_resolution.html new file mode 100644 index 0000000..2c57290 --- /dev/null +++ b/AISuite_Demos/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/app/src/main/assets/product_similaritythreshold.html b/AISuite_Demos/Project 2/app/src/main/assets/product_similaritythreshold.html new file mode 100644 index 0000000..776cc4b --- /dev/null +++ b/AISuite_Demos/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/app/src/main/ic_launcher-playstore.png b/AISuite_Demos/Project 2/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..4a7b683 Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/ic_launcher-playstore.png differ diff --git a/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/MainActivity.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/MainActivity.kt new file mode 100644 index 0000000..5bf92cb --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/MainActivity.kt @@ -0,0 +1,73 @@ +package com.zebra.aidatacapturedemo + +import android.Manifest +import android.content.Context +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberMultiplePermissionsState +import com.zebra.aidatacapturedemo.model.FileUtils +import com.zebra.aidatacapturedemo.ui.theme.AIDataCaptureDemoTheme +import com.zebra.aidatacapturedemo.ui.view.AIDataCaptureDemoApp +import com.zebra.aidatacapturedemo.ui.view.FeedbackUtils +import com.zebra.aidatacapturedemo.viewmodel.AIDataCaptureDemoViewModel + +/** + * MainActivity is the entry point of the AI Data Capture Demo application. + * It sets up the UI and handles permissions. + */ +@OptIn(ExperimentalPermissionsApi::class) +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val pendoApiKey = BuildConfig.PendoApiKey + if (pendoApiKey.isNotEmpty()) { + Log.d(TAG, "pendoApiKey is NotEmpty") + PendoInitializer.init(application, pendoApiKey, true) + } + + FileUtils(application.filesDir.absolutePath, application as Context) + val viewModel: AIDataCaptureDemoViewModel by viewModels { AIDataCaptureDemoViewModel.factory() } + FeedbackUtils(viewModel, application as Context) + + val activityLifecycle = lifecycle + setContent { + + val multiplePermissionsState = rememberMultiplePermissionsState( + listOf( + Manifest.permission.CAMERA, + Manifest.permission.RECORD_AUDIO + ) + ) + LaunchedEffect(Unit) { + // Request the permission when the Composable first enters the composition + multiplePermissionsState.launchMultiplePermissionRequest() + } + + AIDataCaptureDemoTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { activityInnerPadding -> + 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/app/src/main/java/com/zebra/aidatacapturedemo/PendoInitializer.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt new file mode 100644 index 0000000..155ccbc --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/AIDataCaptureDemoUiState.kt @@ -0,0 +1,217 @@ +// 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"), + Expiration("Expiration Date Parser") +} + +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 = "", + var extractedExpirationDate: String? = null, + var detectedExpirationDates: List = emptyList() +) + +/** + * 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 extractedExpirationDate: String? = null, + var detectedExpirationDates: List = emptyList(), + var isExpirationMode: Boolean = false, + + var selectedFilterType: FilterType = FilterType.NONE, + var ocrFilterData: OcrFilterData = FileUtils.loadOcrFilterData(), + var barcodeFilterData: BarcodeFilterData = FileUtils.loadBarcodeFilterData() +) + diff --git a/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/CustomerInfo.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/CustomerInfo.kt new file mode 100644 index 0000000..e392831 --- /dev/null +++ b/AISuite_Demos/Project 2/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/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/data/FilterData.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/data/ModuleData.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/data/ProductData.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/data/ResultData.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/model/BarcodeAnalyzer.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/model/CustomClosedFloatingPointRangeAdapter.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/model/ExpirationDateParser.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/ExpirationDateParser.kt new file mode 100644 index 0000000..df0c88d --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/ExpirationDateParser.kt @@ -0,0 +1,216 @@ +package com.zebra.aidatacapturedemo.model + +import com.zebra.aidatacapturedemo.data.ResultData +import java.util.Calendar +import java.util.regex.Pattern + +/** + * Utility class to extract expiration dates from OCR text results based on product label analysis rules. + */ +object ExpirationDateParser { + + val KEYWORDS = listOf( + "EXP", "EXPIRY", "EXPIRES", "EXPIRATION DATE", + "BEST BEFORE", "BEST BY", "BB", "BBE", "USE BY", "UB", "USEBEFORE", + "MA", "MFG", "LOT", "ED" + ) + + private val FULL_MONTH_NAMES = listOf( + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ) + + // Expanded date pattern to support spaces and dots + const val DATE_PATTERN_STR = + """(\d{1,2}\s*[./\-\s]\s*\d{2,4}|\d{4}\s*[./\-\s]\s*\d{1,2}|(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)\.?\s*\d{2,4}|\d{4}\s*-\s*\d{2}\s*-\s*\d{2})""" + + val KEYWORD_PATTERN_STR = KEYWORDS.joinToString("|") { Pattern.quote(it) } + + enum class DateStatus { + GREEN, YELLOW, RED, NONE + } + + private fun getCurrentYearAndMonth(): Pair { + val cal = Calendar.getInstance() + return cal.get(Calendar.YEAR) to (cal.get(Calendar.MONTH) + 1) + } + + private fun parseDate(ocrText: String): Pair? { + FULL_MONTH_NAMES.forEachIndexed { index, monthName -> + val formattedRegex = Regex("$monthName\\s+(\\d{4})", RegexOption.IGNORE_CASE) + val formattedMatch = formattedRegex.find(ocrText) + if (formattedMatch != null) { + val year = formattedMatch.groups[1]?.value?.toInt() ?: 0 + if (year in 2020..2045) return (index + 1) to year + } + } + + // Strip prefixes to evaluate date strings cleanly + val cleanOcrText = ocrText.replace("The Expiration Date is: ", "", ignoreCase = true) + val dateRegex = Regex(DATE_PATTERN_STR, RegexOption.IGNORE_CASE) + val match = dateRegex.find(cleanOcrText) ?: return null + val dateStr = match.value + + try { + // 1. Check for YYYY/MM, YYYY.MM, or YYYY-MM structures first + val yyyyMmRegex = Regex("""(\d{4})\s*[./\-\s]\s*(\d{1,2})""") + val yyyyMmMatch = yyyyMmRegex.find(dateStr) + if (yyyyMmMatch != null) { + val year = yyyyMmMatch.groups[1]?.value?.toInt() ?: 0 + val month = yyyyMmMatch.groups[2]?.value?.toInt() ?: 0 + if (year in 2020..2045 && month in 1..12) return month to year + } + + // 2. Standard MM/YY or MM/YYYY or MM-YY or MM-YYYY or MM.YY + val slashRegex = Regex("""(\d{1,2})\s*[./\-\s]\s*(\d{2,4})""") + val slashMatch = slashRegex.find(dateStr) + if (slashMatch != null) { + val month = slashMatch.groups[1]?.value?.toInt() ?: 0 + val yearStr = slashMatch.groups[2]?.value ?: "" + val year = if (yearStr.length == 2) 2000 + yearStr.toInt() else yearStr.toInt() + + if (year in 2020..2045 && month in 1..12) return month to year + } + + // 3. MON YYYY or MON YY + val monRegex = Regex("""(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)\.?\s*(\d{2,4})""", RegexOption.IGNORE_CASE) + val monMatch = monRegex.find(dateStr) + if (monMatch != null) { + val months = listOf("JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC") + val month = months.indexOf(monMatch.groups[1]?.value?.uppercase()) + 1 + val yearStr = monMatch.groups[2]?.value ?: "" + + val year = if (yearStr.length == 2) 2000 + yearStr.toInt() else yearStr.toInt() + if (year in 2020..2045 && month in 1..12) return month to year + } + + // 4. YYYY-MM-DD + val isoRegex = Regex("""(\d{4})\s*-\s*(\d{2})\s*-\s*(\d{2})""") + val isoMatch = isoRegex.find(dateStr) + if (isoMatch != null) { + val year = isoMatch.groups[1]?.value?.toInt() ?: 0 + val month = isoMatch.groups[2]?.value?.toInt() ?: 0 + + if (year in 2020..2045 && month in 1..12) return month to year + } else { + val shortIsoRegex = Regex("""(\d{4})\s*-\s*(\d{2})""") + val shortIsoMatch = shortIsoRegex.find(dateStr) + if (shortIsoMatch != null) { + val year = shortIsoMatch.groups[1]?.value?.toInt() ?: 0 + val month = shortIsoMatch.groups[2]?.value?.toInt() ?: 0 + + if (year in 2020..2045 && month in 1..12) return month to year + } + } + } catch (e: Exception) { + // Ignore parsing errors + } + return null + } + + fun formatWithMonthName(ocrText: String): String { + val parsed = parseDate(ocrText) ?: return "" + val (month, year) = parsed + if (month in 1..12) { + return "${FULL_MONTH_NAMES[month - 1]} $year" + } + return "" + } + + fun isDateLike(text: String): Boolean { + val textUpper = text.uppercase() + val hasKeyword = KEYWORDS.any { textUpper.contains(it) } + val hasDatePattern = Regex(DATE_PATTERN_STR, RegexOption.IGNORE_CASE).containsMatchIn(text) + return hasKeyword || hasDatePattern + } + + fun getDateStatus(ocrText: String): DateStatus { + val parsed = parseDate(ocrText) ?: return DateStatus.NONE + val (month, year) = parsed + val (currentYear, currentMonth) = getCurrentYearAndMonth() + + if (year > currentYear) return DateStatus.GREEN + if (year == currentYear) { + return when { + month > currentMonth + 1 -> DateStatus.GREEN + month >= currentMonth -> DateStatus.YELLOW + else -> DateStatus.RED + } + } + return DateStatus.RED + } + + fun getMonthsUntilExpiration(ocrText: String): Int { + val parsed = parseDate(ocrText) ?: return 0 + val (month, year) = parsed + val (currentYear, currentMonth) = getCurrentYearAndMonth() + + return (year - currentYear) * 12 + (month - currentMonth) + } + + fun extractExpirationDate(ocrText: String): String { + if (ocrText.isBlank()) return "Not found" + val regex = Regex("(?i)($KEYWORD_PATTERN_STR)[.:\\s]*$DATE_PATTERN_STR", RegexOption.IGNORE_CASE) + val matches = regex.findAll(ocrText) + val results = mutableListOf() + + for (match in matches) { + val date = formatWithMonthName(match.value) + if (date.isNotEmpty()) { + results.add("The Expiration Date is: $date") + } + } + + if (results.isEmpty()) return "Not found" + return results.first() + } + + fun extractAllFormattedFromResults(results: List): List { + if (results.isEmpty()) return emptyList() + val foundDates = mutableSetOf() + val combinedRegex = Regex("(?i)($KEYWORD_PATTERN_STR)[.:\\s]*$DATE_PATTERN_STR", RegexOption.IGNORE_CASE) + + for (item in results) { + val matches = combinedRegex.findAll(item.text) + for (match in matches) { + val cleanDate = formatWithMonthName(match.value) + if (cleanDate.isNotEmpty()) { + foundDates.add("The Expiration Date is: $cleanDate") + } + } + } + return foundDates.toList() + } + + fun extractAllFromResults(results: List): List { + if (results.isEmpty()) return emptyList() + val foundDates = mutableSetOf() + val combinedRegex = Regex("(?i)($KEYWORD_PATTERN_STR)[.:\\s]*$DATE_PATTERN_STR", RegexOption.IGNORE_CASE) + val standaloneDateRegex = Regex(DATE_PATTERN_STR, RegexOption.IGNORE_CASE) + + for (item in results) { + val matchesWithKeyword = combinedRegex.findAll(item.text) + var foundWithKeyword = false + for (match in matchesWithKeyword) { + val rawDate = match.value // Using full match to catch keyword context + val cleanDate = formatWithMonthName(rawDate) + if (cleanDate.isNotEmpty()) { + foundDates.add("The Expiration Date is: $cleanDate") + foundWithKeyword = true + } + } + + if (!foundWithKeyword) { + val matchesStandalone = standaloneDateRegex.findAll(item.text) + for (match in matchesStandalone) { + val rawDate = match.value + val cleanDate = formatWithMonthName(rawDate) + if (cleanDate.isNotEmpty()) { + foundDates.add("The Expiration Date is: $cleanDate") + } + } + } + } + return foundDates.toList() + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/FileUtils.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/FileUtils.kt new file mode 100644 index 0000000..92c44f6 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/FileUtils.kt @@ -0,0 +1,455 @@ +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) + } ?: "", + extractedExpirationDate = uiState.extractedExpirationDate + ) + 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/app/src/main/java/com/zebra/aidatacapturedemo/model/FilterUtils.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/model/GenericEntityTrackerAnalyzer.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/model/ProductEnrollmentRecognition.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/model/RetailShelfAnalyzer.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/model/TextOCRAnalyzer.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/TextOCRAnalyzer.kt new file mode 100644 index 0000000..1ecfc08 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/model/TextOCRAnalyzer.kt @@ -0,0 +1,301 @@ +// 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 { + // Optional cleanup + } + } + } + + private fun configure() { + try { + if (uiState.value.usecaseSelected == UsecaseState.OCRBarcodeFind.value) { + 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 + + // Optimized adjustments for OCR Find pipeline + textOCRSettings.unclipRatio = 2.0f + textOCRSettings.minRatioForRotation = 0.1f + } else { + 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 - Tuned for Curved & Vertical Medical Labels + 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() + + // FIXED: Increased unclip margin to capture complete warped text curves near margins + textOCRSettings.unclipRatio = 2.2f + + // FIXED: Drastically lowered rotation filter limit to process full vertical text frames + textOCRSettings.minRatioForRotation = 0.05f + + 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) { + // FIXED: Expanded spatial thresholds so curved words aren't mistakenly separated + textOCRSettings.grouping.widthDistanceRatio = 2.2f + textOCRSettings.grouping.heightDistanceRatio = 2.5f + textOCRSettings.grouping.centerDistanceRatio = 0.8f + textOCRSettings.grouping.paragraphHeightDistance = 1.5f + textOCRSettings.grouping.paragraphHeightRatioThreshold = 0.45f + } else { + // Reset to stable defaults optimized for cylindrical label setups + textOCRSettings.grouping.widthDistanceRatio = 2.0f + textOCRSettings.grouping.heightDistanceRatio = 2.2f + textOCRSettings.grouping.centerDistanceRatio = 0.7f + textOCRSettings.grouping.paragraphHeightDistance = 1.2f + textOCRSettings.grouping.paragraphHeightRatioThreshold = 0.35f + } + } + } 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/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Color.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Theme.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/theme/Type.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureDemoApp.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureModalNavigationDrawer.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureModalNavigationDrawer.kt new file mode 100644 index 0000000..223d70c --- /dev/null +++ b/AISuite_Demos/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 Mobile Computing AI Suite", + 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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureStartScreen.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureStartScreen.kt new file mode 100644 index 0000000..da40e37 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AIDataCaptureStartScreen.kt @@ -0,0 +1,420 @@ +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.ocr_icon, + stringResource(id = R.string.expiration_demo), + stringResource(id = R.string.expiration_desc), + Variables.mainIcon1, + Variables.secondaryIcon1, + onItemClick = { selectedUsecase -> + viewModel.updateAppBarTitle(getString(context, R.string.expiration_demo)) + viewModel.updateSelectedUsecase(selectedUsecase) + viewModel.setExpirationMode(true) // Automatically enable expiration mode + 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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AboutScreen.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/AboutScreen.kt new file mode 100644 index 0000000..b92aba6 --- /dev/null +++ b/AISuite_Demos/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 Mobile Computing AI Suite 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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapPickingScreen.kt new file mode 100644 index 0000000..820e903 --- /dev/null +++ b/AISuite_Demos/Project 2/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/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt new file mode 100644 index 0000000..579e15e --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeMapResultScreen.kt @@ -0,0 +1,314 @@ +// 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.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.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 +import androidx.compose.ui.graphics.drawscope.DrawScope +import com.zebra.aidatacapturedemo.data.ResultData +import kotlin.math.abs + +/** + * BarcodeMapResultScreen is a Composable function that displays an abstract + * geometrical layout of detected barcodes 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 Layout 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 + val scaler = min( + displayTotalWidthInPx.toFloat() / capturedBitmap.width.toFloat(), + availableHeightInPx / capturedBitmap.height.toFloat() + ) + val gapX = (displayTotalWidthInPx - (scaler * capturedBitmap.width.toFloat())) / 2f + val gapY = (availableHeightInPx - (scaler * capturedBitmap.height.toFloat())) / 2f + + Box( + modifier = Modifier + .fillMaxSize() + .padding( + top = displayStatusBarHeightInDp, + bottom = displayNavigationBarHeightInDp + ) + .background(color = Color(0xFFF0F2F5)) // Clean modern background + ) { + // ABSTRACT MAP CANVAS + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + DrawAbstractBarcodeMap( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + } + + // SUMMARY OVERLAY + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp), + contentAlignment = Alignment.TopCenter + ) { + 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 = 32.dp), + contentAlignment = Alignment.BottomCenter + ) { + Button( + onClick = { + viewModel.saveBarcodeLayout() + navController.navigate(Screen.CustomerInformation.route) + }, + modifier = Modifier + .fillMaxWidth(0.6f) + .height(54.dp), + shape = RoundedCornerShape(27.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF006D39)) // Dark green + ) { + Text( + text = "Save Barcode Map", + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = Color.White + ) + ) + } + } + } + } +} + +@Composable +private fun DrawAbstractBarcodeMap( + uiState: AIDataCaptureDemoUiState, + scaler: Float, + gapX: Float, + gapY: Float, + displayMetricsDensity: Float +) { + 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) + } + } + } + + 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() + + var currentLeftX = -1f + + 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 + } + } + + val scaledLeft = (scaler * left) + gapX + val scaledTop = (scaler * (avgCenterY - avgHeight/2)) + gapY + val scaledWidth = (scaler * bBoxWidth) + val scaledHeight = (scaler * avgHeight) + + 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 + } + } + } +} + +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) + + // 1. Draw simple geometrical shape (Rectangle) + drawRect( + color = themeColor.copy(alpha = 0.2f), + topLeft = topLeft, + size = rectSize + ) + + // 2. Draw sharp outline + drawRect( + color = themeColor, + topLeft = topLeft, + size = rectSize, + style = Stroke(width = 2f * density) + ) + + // 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 + } + + val textX = left + width / 2 + val textY = top + height / 2 - (paint.fontMetrics.ascent + paint.fontMetrics.descent) / 2 + + // 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/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeModelSettings.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeScanPickingScreen.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BarcodeScanPickingScreen.kt new file mode 100644 index 0000000..9478971 --- /dev/null +++ b/AISuite_Demos/Project 2/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/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/BulletHandler.kt b/AISuite_Demos/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/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
  • 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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt new file mode 100644 index 0000000..fb614a6 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CameraPreviewScreen.kt @@ -0,0 +1,1557 @@ +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.fillMaxHeight +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.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.draw.drawWithContent +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +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.text.style.TextAlign +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.FilterType +import com.zebra.aidatacapturedemo.data.OcrRegularFilterOption +import com.zebra.aidatacapturedemo.data.ResultData +import com.zebra.aidatacapturedemo.data.UsecaseState +import com.zebra.aidatacapturedemo.model.ExpirationDateParser +import com.zebra.aidatacapturedemo.ui.view.Screen +import com.zebra.aidatacapturedemo.ui.view.Variables +import com.zebra.aidatacapturedemo.ui.view.FeedbackUtils +import com.zebra.aidatacapturedemo.ui.view.ButtonData +import com.zebra.aidatacapturedemo.ui.view.ButtonWithIconOption +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, + UsecaseState.Expiration.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 -> { + + val expFound = uiState.extractedExpirationDate != null && uiState.extractedExpirationDate != "Not found" + + if (uiState.isBarcodeModelEnabled && !expFound) { + + // 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) { + if (expFound) { + // Ignore everything else and only show Exp in the special UI below + } else { + 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, UsecaseState.Expiration.value -> { + val expFound = uiState.extractedExpirationDate != null && uiState.extractedExpirationDate != "Not found" + if (!expFound) { + 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 -> { + // If the above cases didn't match, check by string literal just in case + if (selectedDemo == "Expiration Date Parser") { + val expFound = uiState.extractedExpirationDate != null && uiState.extractedExpirationDate != "Not found" + // If the date is already found and "locked", stop showing the jumpy OCR boxes + if (!expFound) { + DrawOCRResult( + uiState = uiState, + scaler = scaler, + gapX = gapX, + gapY = gapY, + displayMetricsDensity = displayMetricsDensity + ) + } + } 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) + ) + } + } + + // Expiration Date Stack (Live) + if (uiState.detectedExpirationDates.isNotEmpty()) { + val scrollState = rememberScrollState() + Box( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 80.dp), + contentAlignment = Alignment.BottomCenter + ) { + Box( + modifier = Modifier + .fillMaxWidth(0.95f) + .fillMaxHeight(0.6f) // Increased height to show more items + .background(Color.Black.copy(alpha = 0.3f), RoundedCornerShape(12.dp)) + .padding(bottom = 8.dp) + ) { + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Header with Clear Button + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Found Dates (${uiState.detectedExpirationDates.size})", + style = TextStyle( + color = Color.White, + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + ) + + Box( + modifier = Modifier + .background(Color.Red.copy(alpha = 0.8f), RoundedCornerShape(8.dp)) + .clickable { viewModel.clearDetectedExpirationDates() } + .padding(horizontal = 12.dp, vertical = 4.dp) + ) { + Text( + text = "Clear All", + style = TextStyle(color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.Bold) + ) + } + } + + Box( + modifier = Modifier + .weight(1f) + .padding(horizontal = 8.dp) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .drawWithContent { + drawContent() + // Custom Scrollbar - More Visible + val needScroll = scrollState.maxValue > 0 + if (needScroll) { + val viewPortHeight = this.size.height + val contentHeight = viewPortHeight + scrollState.maxValue + val scrollbarHeight = (viewPortHeight / contentHeight) * viewPortHeight + val scrollbarOffset = (scrollState.value.toFloat() / contentHeight) * viewPortHeight + + drawRect( + color = Color.White, + topLeft = Offset(this.size.width - 4.dp.toPx(), scrollbarOffset), + size = androidx.compose.ui.geometry.Size(4.dp.toPx(), scrollbarHeight) + ) + } + }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + uiState.detectedExpirationDates.forEach { dateText -> + val status = ExpirationDateParser.getDateStatus(dateText) + val buttonColor = when (status) { + ExpirationDateParser.DateStatus.GREEN -> Color(0xFF006D39) + ExpirationDateParser.DateStatus.YELLOW -> Color(0xFFFFC107) + ExpirationDateParser.DateStatus.RED -> Color.Red + else -> Variables.mainPrimary + } + + Column( + modifier = Modifier + .fillMaxWidth() + .background(Color.Black.copy(alpha = 0.7f), RoundedCornerShape(12.dp)) + .padding(12.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + ButtonWithIconOption( + ButtonData( + titleId = R.string.expiration_date, + color = buttonColor, + alpha = 1f, + enabled = true, + onButtonClick = { }, + titleString = dateText + ), + drawableRes = R.drawable.ic_check + ) + + val months = ExpirationDateParser.getMonthsUntilExpiration(dateText) + val message = when (status) { + ExpirationDateParser.DateStatus.GREEN -> if (months > 0) "This medicine will be expired in $months months" else null + ExpirationDateParser.DateStatus.YELLOW -> "This medicine will be expired in 1 month" + ExpirationDateParser.DateStatus.RED -> "This medicine is already expired" + else -> null + } + + if (message != null) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = message, + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + ) + } + } + } + Spacer(modifier = Modifier.height(8.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 + var borderColor = Color(0xFFFF7B00) + if (uiState.isExpirationMode) { + val status = ExpirationDateParser.getDateStatus(ocrResultData.text) + borderColor = when (status) { + ExpirationDateParser.DateStatus.GREEN -> Color.Green + ExpirationDateParser.DateStatus.YELLOW -> Color.Yellow + ExpirationDateParser.DateStatus.RED -> Color.Red + else -> Color(0xFFFF7B00) + } + } + + drawRect( + color = borderColor, + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (4f * 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 + var borderColor = Color(0xFFFF7B00) + if (uiState.isExpirationMode) { + val status = ExpirationDateParser.getDateStatus(ocrResultData.text) + borderColor = when (status) { + ExpirationDateParser.DateStatus.GREEN -> Color.Green + ExpirationDateParser.DateStatus.YELLOW -> Color.Yellow + ExpirationDateParser.DateStatus.RED -> Color.Red + else -> Color(0xFFFF7B00) + } + } + + drawRect( + color = borderColor, + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (4f * 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 + var borderColor = Color(0xFFFF7B00) + if (uiState.isExpirationMode) { + val status = ExpirationDateParser.getDateStatus(ocrResultData.text) + borderColor = when (status) { + ExpirationDateParser.DateStatus.GREEN -> Color.Green + ExpirationDateParser.DateStatus.YELLOW -> Color.Yellow + ExpirationDateParser.DateStatus.RED -> Color.Red + else -> Color(0xFFFF7B00) + } + } + + drawRect( + color = borderColor, + topLeft = topLeftOffset, + size = androidx.compose.ui.geometry.Size(rectangleWidth, rectangleHeight), + style = Stroke(width = (4f * 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.Expiration.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 || uiState.usecaseSelected == UsecaseState.Expiration.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 = if (uiState.isExpirationMode) { + Color.White.copy(alpha = 0.4f) + } else { + Color.Black.copy(alpha = 0.4f) + }, + shape = RoundedCornerShape(percent = 50) + ) + .clickable { + viewModel.setExpirationMode(!uiState.isExpirationMode) + }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = ImageVector.Companion.vectorResource(R.drawable.ic_check), + contentDescription = "Expiration Mode", + modifier = Modifier.size(20.dp), + tint = if (uiState.isExpirationMode) Variables.mainDefault else 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 + ) { + Text( + text = "${uiState.zoomLevel.toInt()}X", + style = TextStyle( + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Variables.stateDefaultEnabled + ) + ) + } + } + } + } +} +@Composable +fun showInformationBox(info: String, topPadding: androidx.compose.ui.unit.Dp) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = topPadding, start = 16.dp, end = 16.dp) + .background(Color.Black.copy(alpha = 0.8f), RoundedCornerShape(8.dp)) + .padding(12.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = info, + style = TextStyle( + color = Color.White, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center + ) + ) + } +} + +@Composable +fun HandleTopInfo(icon: Int, info: String, showInfo: MutableState) { + if (showInfo.value) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, start = 16.dp, end = 16.dp), + contentAlignment = Alignment.TopCenter + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color.Black.copy(alpha = 0.7f), RoundedCornerShape(8.dp)) + .clickable { showInfo.value = false } // Tap to dismiss + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = info, + style = TextStyle( + color = Color.White, + fontSize = 14.sp, + fontWeight = FontWeight.Normal + ), + modifier = Modifier.weight(1f) + ) + } + } + } +} \ No newline at end of file diff --git a/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CommonUIElements.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CommonUIElements.kt new file mode 100644 index 0000000..9c27e7f --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CommonUIElements.kt @@ -0,0 +1,722 @@ +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, + val titleString: String? = null +) + +@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 = buttonData.titleString ?: 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 = buttonData.titleString ?: 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 = buttonData.titleString ?: 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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomBulletSpan.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/CustomerInformationScreen.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt new file mode 100644 index 0000000..5cbc2cb --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoSettingsScreen.kt @@ -0,0 +1,716 @@ +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 -> { + 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", + 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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoStartScreen.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoStartScreen.kt new file mode 100644 index 0000000..f0a8ab4 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/DemoStartScreen.kt @@ -0,0 +1,716 @@ +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, + UsecaseState.Expiration.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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/FeedbackUtils.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt new file mode 100644 index 0000000..15a3ab9 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/GlobalConstants.kt @@ -0,0 +1,388 @@ +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.Expiration.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.Expiration.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.Expiration.value -> { + iconId = R.drawable.ocr_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 -> { + settingsString = R.string.barcode_settings + } + + UsecaseState.BarcodeMap.value -> { + settingsString = R.string.barcode_map_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.Expiration.value -> { + settingsString = R.string.text_ocr_recognizer_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.Expiration.value -> { + settingsString = R.string.expiration_demo + } + + 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.Expiration.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.Expiration.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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/NavigationStack.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultCapturedScreen.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultCapturedScreen.kt new file mode 100644 index 0000000..74c9db7 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultCapturedScreen.kt @@ -0,0 +1,328 @@ +// 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.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.wrapContentHeight +import androidx.compose.foundation.layout.height +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.foundation.shape.RoundedCornerShape +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.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.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.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 coil.compose.rememberAsyncImagePainter +import com.zebra.aidatacapturedemo.R +import com.zebra.aidatacapturedemo.data.AIDataCaptureDemoUiState +import com.zebra.aidatacapturedemo.model.ExpirationDateParser +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 + ) + } + + // Expiration Date Stack + if (uiState.detectedExpirationDates.isNotEmpty()) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 100.dp), + contentAlignment = Alignment.BottomCenter + ) { + Column( + modifier = Modifier + .fillMaxWidth(0.9f) + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + uiState.detectedExpirationDates.forEach { dateText -> + val fullText = "The expiration date is $dateText" + val status = ExpirationDateParser.getDateStatus(dateText) + val buttonColor = when (status) { + ExpirationDateParser.DateStatus.GREEN -> Color(0xFF006D39) + ExpirationDateParser.DateStatus.YELLOW -> Color(0xFFFFC107) + ExpirationDateParser.DateStatus.RED -> Color.Red + else -> Color(0xFF007AFE) + } + + Column( + modifier = Modifier + .background(Color.Black.copy(alpha = 0.8f), RoundedCornerShape(12.dp)) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + ButtonWithIconOption( + ButtonData( + titleId = R.string.expiration_date, + color = buttonColor, + alpha = 1f, + enabled = true, + onButtonClick = { }, + titleString = fullText + ), + drawableRes = R.drawable.ic_check + ) + + val months = ExpirationDateParser.getMonthsUntilExpiration(dateText) + val message = when (status) { + ExpirationDateParser.DateStatus.GREEN -> if (months > 0) "This medicine will be expired in $months months" else null + ExpirationDateParser.DateStatus.YELLOW -> "This medicine will be expired in 1 month" + ExpirationDateParser.DateStatus.RED -> "This medicine is already expired" + else -> null + } + + if (message != null) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = message, + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + ) + } + } + } + } + } + } + + // 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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultScreen.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultScreen.kt new file mode 100644 index 0000000..b22f20e --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRBarcodeResultScreen.kt @@ -0,0 +1,314 @@ +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.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.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +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.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.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) } + val sessionExpirationDate = remember { mutableStateOf(null) } + + BackHandler(enabled = true) { + viewModel.handleBackButton(navController) + } + viewModel.updateAppBarTitle(stringResource(R.string.results)) + + LaunchedEffect(updateResultsTrigger.value) { + if(uiState.ocrBarcodeCaptureSessionCount == uiState.ocrBarcodeCaptureSessionIndex){ + val ocrList = uiState.ocrResults.filter { it.text.isNotEmpty() }.map { + ResultRowData(it.text, it.boundingBox, isBarcode = false) + } + val barcodeList = uiState.barcodeResults.filter { it.text.isNotEmpty() }.map { + ResultRowData(it.text, it.boundingBox, isBarcode = true) + } + resultList.clear() + sessionExpirationDate.value = uiState.extractedExpirationDate + if (sessionExpirationDate.value == null || sessionExpirationDate.value == "Not found") { + resultList += ocrList + barcodeList + } + } else { + if (updateResults.value) { + val sessionJson = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { + 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() + } + resultList.clear() + sessionExpirationDate.value = sessionJson?.extractedExpirationDate + if (sessionExpirationDate.value == null || sessionExpirationDate.value == "Not found") { + resultList += ocrList + barcodeList + } + Log.d(TAG, "loadSessionResults = ${resultList.size}") + updateResults.value = false + } + } + } + Column(modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + bottom = innerPadding.calculateBottomPadding() + )) { + + Header(uiState.ocrBarcodeCaptureSessionIndex) + + // Expiration Date Button and Text + var showExpMessage by remember { mutableStateOf(false) } + if (sessionExpirationDate.value != null && sessionExpirationDate.value != "Not found") { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .background(Color.Black.copy(alpha = 0.05f), RoundedCornerShape(12.dp)) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + ButtonWithIconOption( + ButtonData( + titleId = R.string.expiration_date, + color = Variables.mainPrimary, + alpha = 1f, + enabled = true, + onButtonClick = { showExpMessage = !showExpMessage } + ), + drawableRes = R.drawable.ic_check + ) + + if (showExpMessage) { + Spacer(modifier = Modifier.height(16.dp)) + Box( + modifier = Modifier + .background(Variables.mainPrimary, RoundedCornerShape(8.dp)) + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Text( + text = sessionExpirationDate.value ?: "", + style = TextStyle( + fontSize = 32.sp, + fontWeight = FontWeight.ExtraBold, + color = Color.White, + textAlign = TextAlign.Center + ) + ) + } + } + } + } + } else { + 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 + .fillMaxWidth() + .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 = 16.sp, + lineHeight = 20.sp, + fontFamily = FontFamily(Font(R.font.ibm_plex_sans)), + fontWeight = FontWeight(500), + color = Variables.colorsMainSubtle, + letterSpacing = 0.sp, + ), + 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 + if (index >= 1) { + viewModel.updateOcrBarcodeCaptureSessionIndex(index) + updateResultsChanged(true) + incrementResultsTrigger() + } + }) + RoundedIconButton(R.drawable.ic_nextsession, onClick = { + val index = uiState.ocrBarcodeCaptureSessionIndex + 1 + if (index <= uiState.ocrBarcodeCaptureSessionCount) { + 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 + } + } + ), + R.drawable.ic_scan + ) + } + } +} diff --git a/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/OCRModelSettings.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/ProductsResultCapturedScreen.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/RetailModelSettings.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/SettingsMoreInfoScreen.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/SingleResultScreen.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/BarcodeFindFilterHomeScreen.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/CharacterMatchFilterScreen.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/CharacterTypeFilterScreen.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/OCRFindFilterHomeScreen.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/RegexFilterScreen.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/ui/view/filters/StringLengthFilterScreen.kt b/AISuite_Demos/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/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/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt new file mode 100644 index 0000000..aab4384 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/java/com/zebra/aidatacapturedemo/viewmodel/AIDataCaptureDemoViewModel.kt @@ -0,0 +1,2224 @@ +// 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.ExpirationDateParser +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() + + // Map to track persistence of detected dates: Date String -> Count of frames seen + private val datePersistenceMap = mutableMapOf() + private val PERSISTENCE_THRESHOLD = 3 + + 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, UsecaseState.Expiration.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, UsecaseState.Expiration.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.Expiration.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, + isExpirationMode = (usecase == UsecaseState.Expiration.value), + extractedExpirationDate = null, // Reset expiration data on usecase change + detectedExpirationDates = emptyList(), + ocrResults = emptyList(), + barcodeResults = emptyList() + ) + } + } + + /** + * 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, UsecaseState.Expiration.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 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 setExpirationMode(enabled: Boolean) { + _uiState.update { currentState -> + currentState.copy( + isExpirationMode = enabled, + extractedExpirationDate = null // Reset detection when entering/exiting mode + ) + } + if (!enabled) { + datePersistenceMap.clear() + } + } + + fun updateOcrResultData(results: List?) { + val allResults = results ?: listOf() + val formattedDates = mutableListOf() + + // 1. Pull matches from text blocks directly using the improved parser + val individualMatches = com.zebra.aidatacapturedemo.model.ExpirationDateParser.extractAllFormattedFromResults(allResults) + formattedDates.addAll(individualMatches) + + // 2. Multi-line block stitching matching + val combinedText = allResults.joinToString(" ") { it.text } + + val keywords = com.zebra.aidatacapturedemo.model.ExpirationDateParser.KEYWORDS + val datePatternStr = com.zebra.aidatacapturedemo.model.ExpirationDateParser.DATE_PATTERN_STR + val keywordPatternStr = com.zebra.aidatacapturedemo.model.ExpirationDateParser.KEYWORD_PATTERN_STR + val combinedRegex = Regex("(?i)($keywordPatternStr)[.:\\s]*$datePatternStr", RegexOption.IGNORE_CASE) + + val matches = combinedRegex.findAll(combinedText) + for (match in matches) { + val cleanDate = com.zebra.aidatacapturedemo.model.ExpirationDateParser.formatWithMonthName(match.value) + if (cleanDate.isNotEmpty()) { + val fullString = "The Expiration Date is: $cleanDate" + if (!formattedDates.contains(fullString)) { + formattedDates.add(fullString) + } + } + } + + // Secondary fallback check: Scan for standalone dates in the combined frame + val standaloneDateRegex = Regex(datePatternStr, RegexOption.IGNORE_CASE) + val standaloneMatches = standaloneDateRegex.findAll(combinedText) + for (match in standaloneMatches) { + val cleanDate = com.zebra.aidatacapturedemo.model.ExpirationDateParser.formatWithMonthName(match.value) + if (cleanDate.isNotEmpty()) { + val fullString = "The Expiration Date is: $cleanDate" + if (!formattedDates.contains(fullString)) { + formattedDates.add(fullString) + } + } + } + + // Stricter filtering for boxes: only show boxes if it looks like a date or keyword + var filteredResults = allResults + if (uiState.value.isExpirationMode) { + filteredResults = allResults.filter { result -> + com.zebra.aidatacapturedemo.model.ExpirationDateParser.isDateLike(result.text) + } + } + + // Persistence filtering: Update counts for seen dates + formattedDates.forEach { date -> + datePersistenceMap[date] = (datePersistenceMap[date] ?: 0) + 1 + } + + // Only promote dates that have been seen consistently across multiple frames + val persistentDates = datePersistenceMap.filter { it.value >= PERSISTENCE_THRESHOLD }.keys.toList() + + _uiState.update { currentState -> + // We use the persistent dates list as the source of truth for the UI + val updatedList = (currentState.detectedExpirationDates + persistentDates).distinct() + + currentState.copy( + ocrResults = filteredResults, + detectedExpirationDates = updatedList, + extractedExpirationDate = if (persistentDates.isNotEmpty()) persistentDates.last() else currentState.extractedExpirationDate + ) + } + } +// 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) + } else if (currentScreen == Screen.BarcodeMapPicking) { + updateSelectedToteId(null) + } + 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, UsecaseState.Expiration.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, UsecaseState.Expiration.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, UsecaseState.Expiration.value -> { + ocrAnalyzer!!.executeHighRes(highResBitmap) + } + } + } + + 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 updateSelectedToteId(id: String?) { + _uiState.update { currentState -> + currentState.copy( + selectedToteId = id + ) + } + } + + 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) + } + + fun clearDetectedExpirationDates() { + _uiState.update { currentState -> + currentState.copy( + detectedExpirationDates = emptyList(), + extractedExpirationDate = null + ) + } + datePersistenceMap.clear() + } +} diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/barcode_icon.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/barcode_icon.xml new file mode 100644 index 0000000..250ccd2 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/barcode_icon.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/camera_icon.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/camera_icon.xml new file mode 100644 index 0000000..7057457 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/camera_icon.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/down_arrow_icon.xml b/AISuite_Demos/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/app/src/main/res/drawable/down_arrow_icon.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/edit_icon.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/edit_icon.xml new file mode 100644 index 0000000..b00f82d --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/edit_icon.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/flashlight_icon.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/flashlight_icon.xml new file mode 100644 index 0000000..ed1ff54 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/flashlight_icon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/hamburger_icon.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/hamburger_icon.xml new file mode 100644 index 0000000..607bee1 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/hamburger_icon.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_barcode_filter_selected.xml b/AISuite_Demos/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/app/src/main/res/drawable/ic_barcode_filter_selected.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_check.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000..4e02734 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_close_black.xml b/AISuite_Demos/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/app/src/main/res/drawable/ic_close_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_filter_default.xml b/AISuite_Demos/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/app/src/main/res/drawable/ic_filter_default.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_filter_selected.xml b/AISuite_Demos/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/app/src/main/res/drawable/ic_filter_selected.xml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_launcher_background.xml b/AISuite_Demos/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/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_launcher_foreground.xml b/AISuite_Demos/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/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_location.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_location.xml new file mode 100644 index 0000000..b05b9dc --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_location.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_menu_barcode.xml b/AISuite_Demos/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/app/src/main/res/drawable/ic_menu_barcode.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_menu_ocr.xml b/AISuite_Demos/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/app/src/main/res/drawable/ic_menu_ocr.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_next.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_next.xml new file mode 100644 index 0000000..d60c8e3 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_next.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_nextsession.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_nextsession.xml new file mode 100644 index 0000000..015c4e0 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_nextsession.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_ocr_filter_selected.xml b/AISuite_Demos/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/app/src/main/res/drawable/ic_ocr_filter_selected.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_plus.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..ac12f18 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_previoussession.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_previoussession.xml new file mode 100644 index 0000000..572b5d9 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_previoussession.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_right_exapand.xml b/AISuite_Demos/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/app/src/main/res/drawable/ic_right_exapand.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_scan.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_scan.xml new file mode 100644 index 0000000..d7049d3 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_scan.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ic_trash_can.xml b/AISuite_Demos/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/app/src/main/res/drawable/ic_trash_can.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/icon_add.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/icon_add.xml new file mode 100644 index 0000000..01dd990 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/icon_add.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/icon_arrow_forward.xml b/AISuite_Demos/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/app/src/main/res/drawable/icon_arrow_forward.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/icon_close.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/icon_close.xml new file mode 100644 index 0000000..cb9642a --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/icon_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/mic_icon.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/mic_icon.xml new file mode 100644 index 0000000..30f5ed0 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/mic_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ocr_finder_icon.xml b/AISuite_Demos/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/app/src/main/res/drawable/ocr_finder_icon.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/ocr_icon.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/ocr_icon.xml new file mode 100644 index 0000000..24dff58 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/ocr_icon.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/product_enrollment_recognition_icon.xml b/AISuite_Demos/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/app/src/main/res/drawable/product_enrollment_recognition_icon.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/retail_shelf_icon.xml b/AISuite_Demos/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/app/src/main/res/drawable/retail_shelf_icon.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/satisfied_icon.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/satisfied_icon.xml new file mode 100644 index 0000000..ed40c8f --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/satisfied_icon.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/settings_icon.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/settings_icon.xml new file mode 100644 index 0000000..a56c631 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/settings_icon.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/shutter_button.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/shutter_button.xml new file mode 100644 index 0000000..5594cd9 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/shutter_button.xml @@ -0,0 +1,14 @@ + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/technology_icon.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/technology_icon.xml new file mode 100644 index 0000000..090395e --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/technology_icon.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/usecase_icon.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/usecase_icon.xml new file mode 100644 index 0000000..b32dad2 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/usecase_icon.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/video_icon.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/video_icon.xml new file mode 100644 index 0000000..5aa945e --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/video_icon.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/warning_icon.xml b/AISuite_Demos/Project 2/app/src/main/res/drawable/warning_icon.xml new file mode 100644 index 0000000..f673737 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/drawable/warning_icon.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/drawable/zebra_logo_icon.xml b/AISuite_Demos/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/app/src/main/res/drawable/zebra_logo_icon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/AISuite_Demos/Project 2/app/src/main/res/font/ibm_plex_sans.xml b/AISuite_Demos/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/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/app/src/main/res/font/ibm_plex_sans_bold.ttf b/AISuite_Demos/Project 2/app/src/main/res/font/ibm_plex_sans_bold.ttf new file mode 100644 index 0000000..258c10a Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/font/ibm_plex_sans_bold.ttf differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/font/ibm_plex_sans_medium.ttf b/AISuite_Demos/Project 2/app/src/main/res/font/ibm_plex_sans_medium.ttf new file mode 100644 index 0000000..fb75072 Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/font/ibm_plex_sans_medium.ttf differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/font/ibm_plex_sans_regular.ttf b/AISuite_Demos/Project 2/app/src/main/res/font/ibm_plex_sans_regular.ttf new file mode 100644 index 0000000..5387ad4 Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/font/ibm_plex_sans_regular.ttf differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/AISuite_Demos/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/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/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/AISuite_Demos/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/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/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..810633e Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp new file mode 100644 index 0000000..2c0847e Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..01d890e Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..a4bca0e Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..cba5bce Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp new file mode 100644 index 0000000..92e3a1a Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..0b64729 Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..a25b535 Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..14f862f Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp new file mode 100644 index 0000000..d0f3046 Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..aee60ee Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..d3c4f65 Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..85b0afe Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp new file mode 100644 index 0000000..a49f6d4 Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..1d8cb8e Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9e4e2bb Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..81d51a3 Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp new file mode 100644 index 0000000..d43e8c6 Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..bf19fa5 Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..fde2bde Binary files /dev/null and b/AISuite_Demos/Project 2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/AISuite_Demos/Project 2/app/src/main/res/values/colors.xml b/AISuite_Demos/Project 2/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/AISuite_Demos/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/app/src/main/res/values/strings.xml b/AISuite_Demos/Project 2/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..8255579 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/values/strings.xml @@ -0,0 +1,228 @@ + + AI Data Capture Demo + About + + Use Case Demos + OCR & Barcode Find + Search for specific text or barcode + Expiration Date Parser + Specifically detect and parse expiration dates + 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 + Barcode Map 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 + Expiration Date + \ No newline at end of file diff --git a/AISuite_Demos/Project 2/app/src/main/res/values/themes.xml b/AISuite_Demos/Project 2/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..1677006 --- /dev/null +++ b/AISuite_Demos/Project 2/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +