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
- **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.
- **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.
- **Localization**: All app strings are managed in `strings.xml` for easy translation.
- **Modern UI**: Built with Jetpack Compose and Material 3.
- **Permission Handling**: Automatic runtime camera permission requests.
## 🛠️ Requirements
- 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.
## 📱 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.
1. **Connection Bar**: A status pill at the top (Green: Connected, Red: Error, Gray: Checking).
2. **Scanner Guide**: A white square frame indicates the target area with a red alignment line.
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
@@ -42,18 +45,22 @@ CREATE TABLE scans (
Update your database connection details in:
`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
private const val HOST = "YOUR_SERVER_IP"
private const val DATABASE = "YOUR_DB_NAME"
private const val USER = "YOUR_USER"
private const val PASSWORD = "YOUR_PASSWORD"
private const val HOST = "mariadb.peta9.com" // Example: "192.168.1.50"
private const val PORT = "3306"
private const val DATABASE = "sais_db"
private const val USER = "sais_dba"
private const val PASSWORD = "your_password"
```
### 3. Network Access
- Ensure the MariaDB port (default **3306**) is open in your firewall.
- Your MariaDB user must be allowed to connect from remote IPs:
- Ensure the MariaDB port (default **3306**) is open.
- Your MariaDB user must allow remote connections:
```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;
```
@@ -62,12 +69,12 @@ private const val PASSWORD = "YOUR_PASSWORD"
- **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.
- **Coroutines**: For asynchronous database and connection checks.
## ⚠️ Security Warning
> [!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
This project is for educational/personal use.
@@ -11,12 +11,20 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
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.fillMaxWidth
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.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -28,7 +36,10 @@ 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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.example.saipp.data.ScannerRepository
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 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) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
@@ -100,10 +129,11 @@ fun MainScreen() {
}
} else {
Text(
text = "Se requiere permiso de cámara para escanear.",
text = stringResource(R.string.camera_permission_required),
modifier = Modifier.align(Alignment.Center)
)
}
// ... rest of the code
// Popup Dialog for scanned results
scannedBarcode?.let { barcode ->
@@ -117,10 +147,16 @@ fun MainScreen() {
val result = repository.saveBarcode(barcode)
isSaving = false
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 ->
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(
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?") },
title = { Text(text = stringResource(R.string.dialog_title)) },
text = { Text(text = stringResource(R.string.dialog_message, barcode)) },
confirmButton = {
Button(onClick = onConfirm) {
Text("Guardar")
Text(stringResource(R.string.button_save))
}
},
dismissButton = {
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"
// 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 DATABASE = "YOUR_DATABASE_NAME"
private const val USER = "YOUR_USER"
private const val PASSWORD = "YOUR_PASSWORD"
private const val DATABASE = "sais_db"
private const val USER = "sais_dba"
private const val PASSWORD = "Pedro@110387"
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) {
val connection = MariaDbConnection.getConnection()
if (connection == null) {
return@withContext Result.failure(Exception("Failed to connect to database"))
return@withContext Result.failure(Exception("db_connection_error"))
}
try {
@@ -27,7 +27,7 @@ class ScannerRepository {
Log.d(TAG, "Barcode saved successfully: $barcode")
Result.success(Unit)
} else {
Result.failure(Exception("No rows inserted"))
Result.failure(Exception("no_rows_inserted"))
}
} catch (e: SQLException) {
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>
<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>