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"?> <?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>
+1 -1
View File
@@ -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">
+12 -3
View File
@@ -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 ->
Log.d("MainActivity", "Barcode detected: $barcode")
scannedBarcode = barcode
})
}
BarcodeScannerView(onBarcodeDetected = { barcode -> if (isSaving) {
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) {
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,75 +47,150 @@ 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 {
scaleType = PreviewView.ScaleType.FILL_CENTER val options = BarcodeScannerOptions.Builder()
layoutParams = ViewGroup.LayoutParams( .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
ViewGroup.LayoutParams.MATCH_PARENT, .build()
ViewGroup.LayoutParams.MATCH_PARENT 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 imageAnalysis = ImageAnalysis.Builder()
val cameraProvider = cameraProviderFuture.get() .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 { val mediaImage = imageProxy.image
it.setSurfaceProvider(previewView.surfaceProvider) if (mediaImage != null) {
} val image = InputImage.fromMediaImage(
mediaImage,
imageProxy.imageInfo.rotationDegrees
)
val imageAnalysis = ImageAnalysis.Builder() barcodeScanner.process(image)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .addOnSuccessListener { barcodes ->
.build() for (barcode in barcodes) {
.also { analysis -> barcode.rawValue?.let { value ->
analysis.setAnalyzer(cameraExecutor) { imageProxy -> Log.d("BarcodeScanner", "Barcode detected: $value")
val mediaImage = imageProxy.image currentOnBarcodeDetected(value)
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)
}
} }
} }
.addOnFailureListener { e -> }
Log.e("BarcodeScanner", "Scanning failed", e) .addOnFailureListener { e ->
} Log.e("BarcodeScanner", "Scanning failed", e)
.addOnCompleteListener { }
imageProxy.close() .addOnCompleteListener {
} imageProxy.close()
} else { }
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()
)
}
} }