Añadir la lectura de codigo de barras
This commit is contained in:
Generated
+1
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ Android application developed in Kotlin using Jetpack Compose that scans barcode
|
|||||||
## 🚀 Features
|
## 🚀 Features
|
||||||
|
|
||||||
- **Instant Scan**: The camera opens automatically upon application startup.
|
- **Instant Scan**: The camera opens automatically upon application startup.
|
||||||
- **ML Kit Integration**: High-performance barcode detection using Google ML Kit.
|
- **Visual Overlay**: Centered scanning guide with a red line to help align barcodes.
|
||||||
|
- **ML Kit Integration**: High-performance barcode detection (HD 720p analysis).
|
||||||
|
- **Confirmation Flow**: Scanned results are shown in a popup dialog before saving.
|
||||||
- **MariaDB Connectivity**: Direct connection to a remote database via JDBC.
|
- **MariaDB Connectivity**: Direct connection to a remote database via JDBC.
|
||||||
- **Modern UI**: Built with Jetpack Compose and Material 3.
|
- **Modern UI**: Built with Jetpack Compose and Material 3.
|
||||||
- **Permission Handling**: Automatic runtime camera permission requests.
|
- **Permission Handling**: Automatic runtime camera permission requests.
|
||||||
@@ -16,6 +18,13 @@ Android application developed in Kotlin using Jetpack Compose that scans barcode
|
|||||||
- MariaDB Server accessible from the mobile device network.
|
- MariaDB Server accessible from the mobile device network.
|
||||||
- Camera hardware.
|
- Camera hardware.
|
||||||
|
|
||||||
|
## 📱 User Interface
|
||||||
|
|
||||||
|
The app features a professional scanning interface:
|
||||||
|
1. **Scanner Guide**: A white square frame indicates the target area.
|
||||||
|
2. **Detection Feedback**: Once a code is read, a popup dialog appears showing the value.
|
||||||
|
3. **Database Actions**: Users can choose to "Guardar" (Save) to the database or "Cerrar" (Dismiss) to scan again.
|
||||||
|
|
||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
|
|
||||||
### 1. Database Setup
|
### 1. Database Setup
|
||||||
@@ -50,8 +59,8 @@ private const val PASSWORD = "YOUR_PASSWORD"
|
|||||||
|
|
||||||
## 📦 Dependencies
|
## 📦 Dependencies
|
||||||
|
|
||||||
- **CameraX**: For camera preview and frame capture.
|
- **CameraX**: For camera preview and high-resolution frame capture.
|
||||||
- **ML Kit Barcode Scanning**: For processing barcodes.
|
- **ML Kit Barcode Scanning**: For processing all barcode formats.
|
||||||
- **MariaDB Java Client**: JDBC driver for database communication.
|
- **MariaDB Java Client**: JDBC driver for database communication.
|
||||||
- **Coroutines**: For asynchronous database operations.
|
- **Coroutines**: For asynchronous database operations.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.example.saipp
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
@@ -12,9 +13,12 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -71,6 +75,9 @@ fun MainScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scannedBarcode by remember { mutableStateOf<String?>(null) }
|
||||||
|
var isSaving by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -78,27 +85,15 @@ fun MainScreen() {
|
|||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
) {
|
) {
|
||||||
if (hasCameraPermission) {
|
if (hasCameraPermission) {
|
||||||
var isProcessing by remember { mutableStateOf(false) }
|
// Only scan if no barcode is currently being shown in the popup
|
||||||
|
if (scannedBarcode == null) {
|
||||||
BarcodeScannerView(onBarcodeDetected = { barcode ->
|
BarcodeScannerView(onBarcodeDetected = { barcode ->
|
||||||
if (!isProcessing) {
|
Log.d("MainActivity", "Barcode detected: $barcode")
|
||||||
isProcessing = true
|
scannedBarcode = barcode
|
||||||
scope.launch {
|
|
||||||
val result = repository.saveBarcode(barcode)
|
|
||||||
result.onSuccess {
|
|
||||||
Toast.makeText(context, "Guardado: $barcode", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
result.onFailure { e ->
|
|
||||||
Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
// Add a small delay to avoid multiple triggers for the same barcode
|
|
||||||
kotlinx.coroutines.delay(2000)
|
|
||||||
isProcessing = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (isProcessing) {
|
if (isSaving) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
@@ -109,6 +104,51 @@ fun MainScreen() {
|
|||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Popup Dialog for scanned results
|
||||||
|
scannedBarcode?.let { barcode ->
|
||||||
|
ResultDialog(
|
||||||
|
barcode = barcode,
|
||||||
|
onDismiss = { scannedBarcode = null },
|
||||||
|
onConfirm = {
|
||||||
|
scannedBarcode = null
|
||||||
|
isSaving = true
|
||||||
|
scope.launch {
|
||||||
|
val result = repository.saveBarcode(barcode)
|
||||||
|
isSaving = false
|
||||||
|
result.onSuccess {
|
||||||
|
Toast.makeText(context, "Guardado con éxito", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
result.onFailure { e ->
|
||||||
|
Toast.makeText(context, "Error al guardar: ${e.message}", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ResultDialog(
|
||||||
|
barcode: String,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: () -> Unit
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(text = "Código Detectado") },
|
||||||
|
text = { Text(text = "Se ha leído el código:\n\n$barcode\n\n¿Deseas guardarlo en la base de datos?") },
|
||||||
|
confirmButton = {
|
||||||
|
Button(onClick = onConfirm) {
|
||||||
|
Text("Guardar")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("Cerrar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.example.saipp.ui.scanner
|
package com.example.saipp.ui.scanner
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.util.Size
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
@@ -9,15 +10,32 @@ import androidx.camera.core.ImageAnalysis
|
|||||||
import androidx.camera.core.Preview
|
import androidx.camera.core.Preview
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
import androidx.camera.view.PreviewView
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.RoundRect
|
||||||
|
import androidx.compose.ui.graphics.ClipOp
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
|
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||||
|
import com.google.mlkit.vision.barcode.common.Barcode
|
||||||
import com.google.mlkit.vision.common.InputImage
|
import com.google.mlkit.vision.common.InputImage
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
@@ -29,22 +47,35 @@ fun BarcodeScannerView(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val cameraExecutor = remember { Executors.newSingleThreadExecutor() }
|
val cameraExecutor = remember { Executors.newSingleThreadExecutor() }
|
||||||
val barcodeScanner = remember { BarcodeScanning.getClient() }
|
|
||||||
|
|
||||||
AndroidView(
|
// Use updated state to ensure the analyzer always has the latest callback
|
||||||
factory = { ctx ->
|
val currentOnBarcodeDetected by rememberUpdatedState(onBarcodeDetected)
|
||||||
PreviewView(ctx).apply {
|
|
||||||
|
val options = BarcodeScannerOptions.Builder()
|
||||||
|
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
|
||||||
|
.build()
|
||||||
|
val barcodeScanner = remember { BarcodeScanning.getClient(options) }
|
||||||
|
|
||||||
|
val previewView = remember {
|
||||||
|
PreviewView(context).apply {
|
||||||
scaleType = PreviewView.ScaleType.FILL_CENTER
|
scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
update = { previewView ->
|
|
||||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
|
||||||
|
|
||||||
|
// Cleanup resources when the composable is disposed
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
barcodeScanner.close()
|
||||||
|
cameraExecutor.shutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(lifecycleOwner) {
|
||||||
|
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||||
cameraProviderFuture.addListener({
|
cameraProviderFuture.addListener({
|
||||||
val cameraProvider = cameraProviderFuture.get()
|
val cameraProvider = cameraProviderFuture.get()
|
||||||
|
|
||||||
@@ -53,10 +84,18 @@ fun BarcodeScannerView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val imageAnalysis = ImageAnalysis.Builder()
|
val imageAnalysis = ImageAnalysis.Builder()
|
||||||
|
.setTargetResolution(Size(1280, 720)) // HD resolution for better detection
|
||||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||||
.build()
|
.build()
|
||||||
.also { analysis ->
|
.also { analysis ->
|
||||||
|
var lastLogTime = 0L
|
||||||
analysis.setAnalyzer(cameraExecutor) { imageProxy ->
|
analysis.setAnalyzer(cameraExecutor) { imageProxy ->
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (currentTime - lastLogTime > 2000) { // Log every 2 seconds
|
||||||
|
Log.d("BarcodeScanner", "Analyzer: Processing frame. Rotation: ${imageProxy.imageInfo.rotationDegrees}")
|
||||||
|
lastLogTime = currentTime
|
||||||
|
}
|
||||||
|
|
||||||
val mediaImage = imageProxy.image
|
val mediaImage = imageProxy.image
|
||||||
if (mediaImage != null) {
|
if (mediaImage != null) {
|
||||||
val image = InputImage.fromMediaImage(
|
val image = InputImage.fromMediaImage(
|
||||||
@@ -68,7 +107,8 @@ fun BarcodeScannerView(
|
|||||||
.addOnSuccessListener { barcodes ->
|
.addOnSuccessListener { barcodes ->
|
||||||
for (barcode in barcodes) {
|
for (barcode in barcodes) {
|
||||||
barcode.rawValue?.let { value ->
|
barcode.rawValue?.let { value ->
|
||||||
onBarcodeDetected(value)
|
Log.d("BarcodeScanner", "Barcode detected: $value")
|
||||||
|
currentOnBarcodeDetected(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,10 +134,63 @@ fun BarcodeScannerView(
|
|||||||
preview,
|
preview,
|
||||||
imageAnalysis
|
imageAnalysis
|
||||||
)
|
)
|
||||||
|
Log.d("BarcodeScanner", "Camera and Analysis bound successfully")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("BarcodeScanner", "Use case binding failed", e)
|
Log.e("BarcodeScanner", "Use case binding failed", e)
|
||||||
}
|
}
|
||||||
}, ContextCompat.getMainExecutor(context))
|
}, ContextCompat.getMainExecutor(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { previewView },
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
ScannerOverlay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScannerOverlay() {
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
val canvasWidth = size.width
|
||||||
|
val canvasHeight = size.height
|
||||||
|
val boxWidth = canvasWidth * 0.7f
|
||||||
|
val boxHeight = boxWidth
|
||||||
|
val left = (canvasWidth - boxWidth) / 2
|
||||||
|
val top = (canvasHeight - boxHeight) / 2
|
||||||
|
|
||||||
|
val rectPath = Path().apply {
|
||||||
|
addRoundRect(
|
||||||
|
RoundRect(
|
||||||
|
rect = androidx.compose.ui.geometry.Rect(
|
||||||
|
offset = Offset(left, top),
|
||||||
|
size = androidx.compose.ui.geometry.Size(boxWidth, boxHeight)
|
||||||
|
),
|
||||||
|
cornerRadius = CornerRadius(16.dp.toPx())
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clipPath(rectPath, clipOp = ClipOp.Difference) {
|
||||||
|
drawRect(color = Color.Black.copy(alpha = 0.5f))
|
||||||
|
}
|
||||||
|
|
||||||
|
drawRoundRect(
|
||||||
|
color = Color.White,
|
||||||
|
topLeft = Offset(left, top),
|
||||||
|
size = androidx.compose.ui.geometry.Size(boxWidth, boxHeight),
|
||||||
|
cornerRadius = CornerRadius(16.dp.toPx()),
|
||||||
|
style = Stroke(width = 2.dp.toPx())
|
||||||
|
)
|
||||||
|
|
||||||
|
// Back to horizontal line as it's more effective for 1D barcodes
|
||||||
|
drawLine(
|
||||||
|
color = Color.Red,
|
||||||
|
start = Offset(left + 20.dp.toPx(), canvasHeight / 2),
|
||||||
|
end = Offset(left + boxWidth - 20.dp.toPx(), canvasHeight / 2),
|
||||||
|
strokeWidth = 2.dp.toPx()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user