diff --git a/README.md b/README.md index 00c80a3..8f55839 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/app/src/main/java/com/example/saipp/MainActivity.kt b/app/src/main/java/com/example/saipp/MainActivity.kt index 0f0293e..59f197f 100644 --- a/app/src/main/java/com/example/saipp/MainActivity.kt +++ b/app/src/main/java/com/example/saipp/MainActivity.kt @@ -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?>(null) } + var isCheckingConnection by remember { mutableStateOf(true) } + + LaunchedEffect(key1 = true) { + isCheckingConnection = true + connectionStatus = repository.testConnection() + isCheckingConnection = false + } + var scannedBarcode by remember { mutableStateOf(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? +) { + 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 + ) + } + } +} diff --git a/app/src/main/java/com/example/saipp/data/MariaDbConnection.kt b/app/src/main/java/com/example/saipp/data/MariaDbConnection.kt index 908a1df..83f9bf2 100644 --- a/app/src/main/java/com/example/saipp/data/MariaDbConnection.kt +++ b/app/src/main/java/com/example/saipp/data/MariaDbConnection.kt @@ -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" diff --git a/app/src/main/java/com/example/saipp/data/ScannerRepository.kt b/app/src/main/java/com/example/saipp/data/ScannerRepository.kt index eb8097c..0427e8a 100644 --- a/app/src/main/java/com/example/saipp/data/ScannerRepository.kt +++ b/app/src/main/java/com/example/saipp/data/ScannerRepository.kt @@ -13,7 +13,7 @@ class ScannerRepository { suspend fun saveBarcode(barcode: String): Result = 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 = 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")) + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 802774b..87d6a6b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,16 @@ SAIpp + Se requiere permiso de cámara para escanear. + Guardado con éxito + Error al guardar: %1$s + Código Detectado + Se ha leído el código:\n\n%1$s\n\n¿Deseas guardarlo en la base de datos? + Guardar + Cerrar + Comprobando servidor... + Conectado a servidor + Sin conexión al servidor + No se pudo establecer conexión con el servidor + Error de conexión a la base de datos + No se insertaron filas en la base de datos \ No newline at end of file