diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..ca16a99
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/planningMode.xml b/.idea/planningMode.xml
new file mode 100644
index 0000000..356d457
--- /dev/null
+++ b/.idea/planningMode.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a90b06a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,64 @@
+# SAIpp - Barcode Scanner & MariaDB Connector
+
+Android application developed in Kotlin using Jetpack Compose that scans barcodes and saves them directly to a self-hosted MariaDB database.
+
+## 🚀 Features
+
+- **Instant Scan**: The camera opens automatically upon application startup.
+- **ML Kit Integration**: High-performance barcode detection using Google ML Kit.
+- **MariaDB Connectivity**: Direct connection to a remote database via JDBC.
+- **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.
+- Camera hardware.
+
+## ⚙️ Configuration
+
+### 1. Database Setup
+Ensure your MariaDB database has a table named `scans`. You can use the following SQL:
+
+```sql
+CREATE TABLE scans (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ barcode VARCHAR(255) NOT NULL,
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+### 2. App Credentials
+Update your database connection details in:
+`app/src/main/java/com/example/saipp/data/MariaDbConnection.kt`
+
+```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"
+```
+
+### 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:
+ ```sql
+ GRANT ALL PRIVILEGES ON YOUR_DB_NAME.* TO 'YOUR_USER'@'%' IDENTIFIED BY 'YOUR_PASSWORD';
+ FLUSH PRIVILEGES;
+ ```
+
+## 📦 Dependencies
+
+- **CameraX**: For camera preview and frame capture.
+- **ML Kit Barcode Scanning**: For processing barcodes.
+- **MariaDB Java Client**: JDBC driver for database communication.
+- **Coroutines**: For asynchronous database operations.
+
+## ⚠️ 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.
+
+## 📄 License
+This project is for educational/personal use.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b8c4087..c853d6e 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -35,6 +35,11 @@ android {
buildFeatures {
compose = true
}
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
}
dependencies {
@@ -46,6 +51,20 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
+
+ // CameraX
+ implementation(libs.androidx.camera.core)
+ implementation(libs.androidx.camera.camera2)
+ implementation(libs.androidx.camera.lifecycle)
+ implementation(libs.androidx.camera.view)
+
+ // ML Kit Barcode Scanning
+ implementation(libs.mlkit.barcode.scanning)
+
+ // MariaDB JDBC Driver
+ implementation(libs.mariadb.java.client)
+
testImplementation(libs.junit)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 899629c..d556a51 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,6 +2,10 @@
+
+
+
+
- Greeting(
- name = "Android",
- modifier = Modifier.padding(innerPadding)
- )
- }
+ MainScreen()
}
}
}
}
@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
- Text(
- text = "Hello $name!",
- modifier = modifier
- )
-}
-
-@Preview(showBackground = true)
-@Composable
-fun GreetingPreview() {
- SAIppTheme {
- Greeting("Android")
+fun MainScreen() {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val repository = remember { ScannerRepository() }
+
+ var hasCameraPermission by remember {
+ mutableStateOf(
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.CAMERA
+ ) == PackageManager.PERMISSION_GRANTED
+ )
}
-}
\ No newline at end of file
+
+ val launcher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission(),
+ onResult = { granted ->
+ hasCameraPermission = granted
+ }
+ )
+
+ LaunchedEffect(key1 = true) {
+ if (!hasCameraPermission) {
+ launcher.launch(Manifest.permission.CAMERA)
+ }
+ }
+
+ Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ ) {
+ if (hasCameraPermission) {
+ var isProcessing by remember { mutableStateOf(false) }
+
+ BarcodeScannerView(onBarcodeDetected = { barcode ->
+ 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(
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ } else {
+ Text(
+ text = "Se requiere permiso de cámara para escanear.",
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/saipp/data/MariaDbConnection.kt b/app/src/main/java/com/example/saipp/data/MariaDbConnection.kt
new file mode 100644
index 0000000..908a1df
--- /dev/null
+++ b/app/src/main/java/com/example/saipp/data/MariaDbConnection.kt
@@ -0,0 +1,37 @@
+package com.example.saipp.data
+
+import android.util.Log
+import java.sql.Connection
+import java.sql.DriverManager
+import java.sql.SQLException
+import java.util.Properties
+
+object MariaDbConnection {
+ private const val TAG = "MariaDbConnection"
+
+ // TODO: Replace with your actual database details
+ private const val HOST = "YOUR_HOST_IP"
+ 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 URL = "jdbc:mariadb://$HOST:$PORT/$DATABASE"
+
+ fun getConnection(): Connection? {
+ return try {
+ // Load the driver explicitly if needed
+ Class.forName("org.mariadb.jdbc.Driver")
+
+ val props = Properties()
+ props.setProperty("user", USER)
+ props.setProperty("password", PASSWORD)
+ props.setProperty("connectTimeout", "5000") // 5 seconds timeout
+
+ DriverManager.getConnection(URL, props)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error connecting to MariaDB: ${e.message}", e)
+ null
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/saipp/data/ScannerRepository.kt b/app/src/main/java/com/example/saipp/data/ScannerRepository.kt
new file mode 100644
index 0000000..eb8097c
--- /dev/null
+++ b/app/src/main/java/com/example/saipp/data/ScannerRepository.kt
@@ -0,0 +1,43 @@
+package com.example.saipp.data
+
+import android.util.Log
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.sql.SQLException
+
+class ScannerRepository {
+ companion object {
+ private const val TAG = "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"))
+ }
+
+ try {
+ // TODO: Customize table and column names
+ val sql = "INSERT INTO scans (barcode, timestamp) VALUES (?, CURRENT_TIMESTAMP)"
+ val statement = connection.prepareStatement(sql)
+ statement.setString(1, barcode)
+
+ val rowsInserted = statement.executeUpdate()
+ if (rowsInserted > 0) {
+ Log.d(TAG, "Barcode saved successfully: $barcode")
+ Result.success(Unit)
+ } else {
+ Result.failure(Exception("No rows inserted"))
+ }
+ } catch (e: SQLException) {
+ Log.e(TAG, "Database error: ${e.message}", e)
+ Result.failure(e)
+ } finally {
+ try {
+ connection.close()
+ } catch (e: SQLException) {
+ Log.e(TAG, "Error closing connection: ${e.message}", e)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/saipp/ui/scanner/BarcodeScannerView.kt b/app/src/main/java/com/example/saipp/ui/scanner/BarcodeScannerView.kt
new file mode 100644
index 0000000..1d69dbb
--- /dev/null
+++ b/app/src/main/java/com/example/saipp/ui/scanner/BarcodeScannerView.kt
@@ -0,0 +1,103 @@
+package com.example.saipp.ui.scanner
+
+import android.util.Log
+import android.view.ViewGroup
+import androidx.annotation.OptIn
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ExperimentalGetImage
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.view.PreviewView
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.content.ContextCompat
+import com.google.mlkit.vision.barcode.BarcodeScanning
+import com.google.mlkit.vision.common.InputImage
+import java.util.concurrent.Executors
+
+@OptIn(ExperimentalGetImage::class)
+@Composable
+fun BarcodeScannerView(
+ onBarcodeDetected: (String) -> Unit
+) {
+ val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val cameraExecutor = remember { Executors.newSingleThreadExecutor() }
+ val barcodeScanner = remember { BarcodeScanning.getClient() }
+
+ AndroidView(
+ factory = { ctx ->
+ PreviewView(ctx).apply {
+ scaleType = PreviewView.ScaleType.FILL_CENTER
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ }
+ },
+ modifier = Modifier.fillMaxSize(),
+ update = { previewView ->
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
+
+ cameraProviderFuture.addListener({
+ val cameraProvider = cameraProviderFuture.get()
+
+ val preview = Preview.Builder().build().also {
+ it.setSurfaceProvider(previewView.surfaceProvider)
+ }
+
+ val imageAnalysis = ImageAnalysis.Builder()
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+ .build()
+ .also { analysis ->
+ analysis.setAnalyzer(cameraExecutor) { imageProxy ->
+ val mediaImage = imageProxy.image
+ 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)
+ }
+ .addOnCompleteListener {
+ 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))
+ }
+ )
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0549c8a..c0c4bf5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -8,6 +8,9 @@ lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
kotlin = "2.2.10"
composeBom = "2026.02.01"
+cameraX = "1.6.1"
+mlkitBarcode = "17.3.0"
+mariadbDriver = "3.5.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -24,6 +27,12 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "cameraX" }
+androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraX" }
+androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraX" }
+androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraX" }
+mlkit-barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "mlkitBarcode" }
+mariadb-java-client = { group = "org.mariadb.jdbc", name = "mariadb-java-client", version.ref = "mariadbDriver" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }