Añadir la lectura de codigo de barras
This commit is contained in:
@@ -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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user