Añadir la lectura de codigo de barras

This commit is contained in:
2026-07-31 08:23:44 +02:00
parent e6497704df
commit 9796588648
5 changed files with 228 additions and 85 deletions
+1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<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" />
</component>
<component name="ProjectType">
+12 -3
View File
@@ -5,7 +5,9 @@ Android application developed in Kotlin using Jetpack Compose that scans barcode
## 🚀 Features
- **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.
- **Modern UI**: Built with Jetpack Compose and Material 3.
- **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.
- 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
### 1. Database Setup
@@ -50,8 +59,8 @@ private const val PASSWORD = "YOUR_PASSWORD"
## 📦 Dependencies
- **CameraX**: For camera preview and frame capture.
- **ML Kit Barcode Scanning**: For processing barcodes.
- **CameraX**: For camera preview and high-resolution frame capture.
- **ML Kit Barcode Scanning**: For processing all barcode formats.
- **MariaDB Java Client**: JDBC driver for database communication.
- **Coroutines**: For asynchronous database operations.
@@ -3,6 +3,7 @@ package com.example.saipp
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
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.fillMaxSize
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.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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 ->
Box(
modifier = Modifier
@@ -78,27 +85,15 @@ fun MainScreen() {
.padding(innerPadding)
) {
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 ->
Log.d("MainActivity", "Barcode detected: $barcode")
scannedBarcode = barcode
})
}
BarcodeScannerView(onBarcodeDetected = { barcode ->
if (!isProcessing) {
isProcessing = true
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(
modifier = Modifier.align(Alignment.Center)
)
@@ -109,6 +104,51 @@ fun MainScreen() {
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
import android.util.Log
import android.util.Size
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.camera.core.CameraSelector
@@ -9,15 +10,32 @@ import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
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.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.rememberUpdatedState
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.lifecycle.compose.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
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.common.Barcode
import com.google.mlkit.vision.common.InputImage
import java.util.concurrent.Executors
@@ -29,75 +47,150 @@ fun BarcodeScannerView(
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val cameraExecutor = remember { Executors.newSingleThreadExecutor() }
val barcodeScanner = remember { BarcodeScanning.getClient() }
AndroidView(
factory = { ctx ->
PreviewView(ctx).apply {
scaleType = PreviewView.ScaleType.FILL_CENTER
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
// Use updated state to ensure the analyzer always has the latest callback
val currentOnBarcodeDetected by rememberUpdatedState(onBarcodeDetected)
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
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}
// Cleanup resources when the composable is disposed
DisposableEffect(Unit) {
onDispose {
barcodeScanner.close()
cameraExecutor.shutdown()
}
}
LaunchedEffect(lifecycleOwner) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
},
modifier = Modifier.fillMaxSize(),
update = { previewView ->
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(1280, 720)) // HD resolution for better detection
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also { analysis ->
var lastLogTime = 0L
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 preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val mediaImage = imageProxy.image
if (mediaImage != null) {
val image = InputImage.fromMediaImage(
mediaImage,
imageProxy.imageInfo.rotationDegrees
)
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also { analysis ->
analysis.setAnalyzer(cameraExecutor) { imageProxy ->
val mediaImage = imageProxy.image
if (mediaImage != null) {
val image = InputImage.fromMediaImage(
mediaImage,
imageProxy.imageInfo.rotationDegrees
)
barcodeScanner.process(image)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
barcode.rawValue?.let { value ->
onBarcodeDetected(value)
}
barcodeScanner.process(image)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
barcode.rawValue?.let { value ->
Log.d("BarcodeScanner", "Barcode detected: $value")
currentOnBarcodeDetected(value)
}
}
.addOnFailureListener { e ->
Log.e("BarcodeScanner", "Scanning failed", e)
}
.addOnCompleteListener {
imageProxy.close()
}
} else {
imageProxy.close()
}
}
.addOnFailureListener { e ->
Log.e("BarcodeScanner", "Scanning failed", e)
}
.addOnCompleteListener {
imageProxy.close()
}
} else {
imageProxy.close()
}
}
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageAnalysis
)
} catch (e: Exception) {
Log.e("BarcodeScanner", "Use case binding failed", e)
}
}, ContextCompat.getMainExecutor(context))
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageAnalysis
)
Log.d("BarcodeScanner", "Camera and Analysis bound successfully")
} catch (e: Exception) {
Log.e("BarcodeScanner", "Use case binding failed", e)
}
}, 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()
)
}
}