Añadir la lectura de codigo de barras

This commit is contained in:
2026-07-31 09:02:00 +02:00
parent 9796588648
commit 3cd4fc4b53
5 changed files with 138 additions and 26 deletions
+20 -13
View File
@@ -5,25 +5,28 @@ 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.
- **Connection Status**: Real-time indicator at the top showing if the app can reach the MariaDB server.
- **Visual Overlay**: Centered scanning guide with a red line to help align barcodes. - **Visual Overlay**: Centered scanning guide with a red line to help align barcodes.
- **ML Kit Integration**: High-performance barcode detection (HD 720p analysis). - **ML Kit Integration**: High-performance barcode detection (HD 720p analysis).
- **Confirmation Flow**: Scanned results are shown in a popup dialog before saving. - **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.
- **Localization**: All app strings are managed in `strings.xml` for easy translation.
- **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.
## 🛠️ Requirements ## 🛠️ Requirements
- Android device with API 34+ (Android 14+). - Android device with API 34+ (Android 14+).
- MariaDB Server accessible from the mobile device network. - MariaDB Server accessible from the mobile device network on port 3306.
- Camera hardware. - Camera hardware.
## 📱 User Interface ## 📱 User Interface
The app features a professional scanning interface: The app features a professional scanning interface:
1. **Scanner Guide**: A white square frame indicates the target area. 1. **Connection Bar**: A status pill at the top (Green: Connected, Red: Error, Gray: Checking).
2. **Detection Feedback**: Once a code is read, a popup dialog appears showing the value. 2. **Scanner Guide**: A white square frame indicates the target area with a red alignment line.
3. **Database Actions**: Users can choose to "Guardar" (Save) to the database or "Cerrar" (Dismiss) to scan again. 3. **Detection Popup**: When a code is read, a dialog appears with the value.
4. **Database Actions**: Buttons to "Guardar" (Save) or "Cerrar" (Dismiss).
## ⚙️ Configuration ## ⚙️ Configuration
@@ -42,18 +45,22 @@ CREATE TABLE scans (
Update your database connection details in: Update your database connection details in:
`app/src/main/java/com/example/saipp/data/MariaDbConnection.kt` `app/src/main/java/com/example/saipp/data/MariaDbConnection.kt`
> [!IMPORTANT]
> Use the raw domain or IP for the `HOST`. **Do NOT include `https://`**.
```kotlin ```kotlin
private const val HOST = "YOUR_SERVER_IP" private const val HOST = "mariadb.peta9.com" // Example: "192.168.1.50"
private const val DATABASE = "YOUR_DB_NAME" private const val PORT = "3306"
private const val USER = "YOUR_USER" private const val DATABASE = "sais_db"
private const val PASSWORD = "YOUR_PASSWORD" private const val USER = "sais_dba"
private const val PASSWORD = "your_password"
``` ```
### 3. Network Access ### 3. Network Access
- Ensure the MariaDB port (default **3306**) is open in your firewall. - Ensure the MariaDB port (default **3306**) is open.
- Your MariaDB user must be allowed to connect from remote IPs: - Your MariaDB user must allow remote connections:
```sql ```sql
GRANT ALL PRIVILEGES ON YOUR_DB_NAME.* TO 'YOUR_USER'@'%' IDENTIFIED BY 'YOUR_PASSWORD'; GRANT ALL PRIVILEGES ON sais_db.* TO 'sais_dba'@'%' IDENTIFIED BY 'your_password';
FLUSH PRIVILEGES; FLUSH PRIVILEGES;
``` ```
@@ -62,12 +69,12 @@ private const val PASSWORD = "YOUR_PASSWORD"
- **CameraX**: For camera preview and high-resolution frame capture. - **CameraX**: For camera preview and high-resolution frame capture.
- **ML Kit Barcode Scanning**: For processing all barcode formats. - **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 and connection checks.
## ⚠️ Security Warning ## ⚠️ Security Warning
> [!CAUTION] > [!CAUTION]
> This project uses a **direct JDBC connection** from the mobile app to a remote database. This is suitable for personal projects or internal tools, but is **not recommended for production apps** exposed to the public, as it requires storing database credentials in the code and exposing database ports to the internet. > This project uses a **direct JDBC connection**. This is suitable for personal projects or internal tools, but is **not recommended for production apps** as it requires storing credentials in the code and exposing database ports.
## 📄 License ## 📄 License
This project is for educational/personal use. This project is for educational/personal use.
@@ -11,12 +11,20 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -28,7 +36,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.example.saipp.data.ScannerRepository import com.example.saipp.data.ScannerRepository
import com.example.saipp.ui.scanner.BarcodeScannerView import com.example.saipp.ui.scanner.BarcodeScannerView
@@ -75,6 +86,15 @@ fun MainScreen() {
} }
} }
var connectionStatus by remember { mutableStateOf<Result<Unit>?>(null) }
var isCheckingConnection by remember { mutableStateOf(true) }
LaunchedEffect(key1 = true) {
isCheckingConnection = true
connectionStatus = repository.testConnection()
isCheckingConnection = false
}
var scannedBarcode by remember { mutableStateOf<String?>(null) } var scannedBarcode by remember { mutableStateOf<String?>(null) }
var isSaving by remember { mutableStateOf(false) } var isSaving by remember { mutableStateOf(false) }
@@ -93,6 +113,15 @@ fun MainScreen() {
}) })
} }
// Connection Status Indicator at the top
ConnectionIndicator(
modifier = Modifier
.align(Alignment.TopCenter)
.padding(16.dp),
isChecking = isCheckingConnection,
status = connectionStatus
)
if (isSaving) { if (isSaving) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
@@ -100,10 +129,11 @@ fun MainScreen() {
} }
} else { } else {
Text( Text(
text = "Se requiere permiso de cámara para escanear.", text = stringResource(R.string.camera_permission_required),
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) )
} }
// ... rest of the code
// Popup Dialog for scanned results // Popup Dialog for scanned results
scannedBarcode?.let { barcode -> scannedBarcode?.let { barcode ->
@@ -117,10 +147,16 @@ fun MainScreen() {
val result = repository.saveBarcode(barcode) val result = repository.saveBarcode(barcode)
isSaving = false isSaving = false
result.onSuccess { result.onSuccess {
Toast.makeText(context, "Guardado con éxito", Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.save_success, Toast.LENGTH_SHORT).show()
} }
result.onFailure { e -> result.onFailure { e ->
Toast.makeText(context, "Error al guardar: ${e.message}", Toast.LENGTH_LONG).show() val errorMessage = when (e.message) {
"connection_failed" -> context.getString(R.string.db_connection_failed)
"db_connection_error" -> context.getString(R.string.db_connection_error)
"no_rows_inserted" -> context.getString(R.string.no_rows_inserted)
else -> e.message ?: "Unknown error"
}
Toast.makeText(context, context.getString(R.string.save_error, errorMessage), Toast.LENGTH_LONG).show()
} }
} }
} }
@@ -138,17 +174,59 @@ fun ResultDialog(
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text(text = "Código Detectado") }, title = { Text(text = stringResource(R.string.dialog_title)) },
text = { Text(text = "Se ha leído el código:\n\n$barcode\n\n¿Deseas guardarlo en la base de datos?") }, text = { Text(text = stringResource(R.string.dialog_message, barcode)) },
confirmButton = { confirmButton = {
Button(onClick = onConfirm) { Button(onClick = onConfirm) {
Text("Guardar") Text(stringResource(R.string.button_save))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text("Cerrar") Text(stringResource(R.string.button_close))
} }
} }
) )
} }
@Composable
fun ConnectionIndicator(
modifier: Modifier = Modifier,
isChecking: Boolean,
status: Result<Unit>?
) {
Surface(
modifier = modifier,
color = Color.Black.copy(alpha = 0.6f),
shape = CircleShape
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
val color = when {
isChecking -> Color.Gray
status?.isSuccess == true -> Color.Green
else -> Color.Red
}
Surface(
modifier = Modifier.size(8.dp),
color = color,
shape = CircleShape
) {}
Spacer(modifier = Modifier.width(8.dp))
Text(
text = when {
isChecking -> stringResource(R.string.checking_server)
status?.isSuccess == true -> stringResource(R.string.connected_status)
else -> stringResource(R.string.disconnected_status)
},
color = Color.White,
style = MaterialTheme.typography.bodySmall
)
}
}
}
@@ -10,11 +10,11 @@ object MariaDbConnection {
private const val TAG = "MariaDbConnection" private const val TAG = "MariaDbConnection"
// TODO: Replace with your actual database details // TODO: Replace with your actual database details
private const val HOST = "YOUR_HOST_IP" private const val HOST = "mariadb.peta9.com"
private const val PORT = "3306" private const val PORT = "3306"
private const val DATABASE = "YOUR_DATABASE_NAME" private const val DATABASE = "sais_db"
private const val USER = "YOUR_USER" private const val USER = "sais_dba"
private const val PASSWORD = "YOUR_PASSWORD" private const val PASSWORD = "Pedro@110387"
private const val URL = "jdbc:mariadb://$HOST:$PORT/$DATABASE" private const val URL = "jdbc:mariadb://$HOST:$PORT/$DATABASE"
@@ -13,7 +13,7 @@ class ScannerRepository {
suspend fun saveBarcode(barcode: String): Result<Unit> = withContext(Dispatchers.IO) { suspend fun saveBarcode(barcode: String): Result<Unit> = withContext(Dispatchers.IO) {
val connection = MariaDbConnection.getConnection() val connection = MariaDbConnection.getConnection()
if (connection == null) { if (connection == null) {
return@withContext Result.failure(Exception("Failed to connect to database")) return@withContext Result.failure(Exception("db_connection_error"))
} }
try { try {
@@ -27,7 +27,7 @@ class ScannerRepository {
Log.d(TAG, "Barcode saved successfully: $barcode") Log.d(TAG, "Barcode saved successfully: $barcode")
Result.success(Unit) Result.success(Unit)
} else { } else {
Result.failure(Exception("No rows inserted")) Result.failure(Exception("no_rows_inserted"))
} }
} catch (e: SQLException) { } catch (e: SQLException) {
Log.e(TAG, "Database error: ${e.message}", e) Log.e(TAG, "Database error: ${e.message}", e)
@@ -40,4 +40,18 @@ class ScannerRepository {
} }
} }
} }
suspend fun testConnection(): Result<Unit> = withContext(Dispatchers.IO) {
val connection = MariaDbConnection.getConnection()
if (connection != null) {
try {
connection.close()
Result.success(Unit)
} catch (e: SQLException) {
Result.failure(e)
}
} else {
Result.failure(Exception("connection_failed"))
}
}
} }
+13
View File
@@ -1,3 +1,16 @@
<resources> <resources>
<string name="app_name">SAIpp</string> <string name="app_name">SAIpp</string>
<string name="camera_permission_required">Se requiere permiso de cámara para escanear.</string>
<string name="save_success">Guardado con éxito</string>
<string name="save_error">Error al guardar: %1$s</string>
<string name="dialog_title">Código Detectado</string>
<string name="dialog_message">Se ha leído el código:\n\n%1$s\n\n¿Deseas guardarlo en la base de datos?</string>
<string name="button_save">Guardar</string>
<string name="button_close">Cerrar</string>
<string name="checking_server">Comprobando servidor...</string>
<string name="connected_status">Conectado a servidor</string>
<string name="disconnected_status">Sin conexión al servidor</string>
<string name="db_connection_failed">No se pudo establecer conexión con el servidor</string>
<string name="db_connection_error">Error de conexión a la base de datos</string>
<string name="no_rows_inserted">No se insertaron filas en la base de datos</string>
</resources> </resources>