initial commit

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2026-01-11 15:43:53 +01:00
commit 953a6739c1
Se han modificado 63 ficheros con 4144 adiciones y 0 borrados

15
.gitignore vendido Archivo normal
Ver fichero

@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

230
README.md Archivo normal
Ver fichero

@@ -0,0 +1,230 @@
# OBD2 Bluetooth - Vehicle Diagnostic Application
A modern Android application for communicating with OBD2 ELM327 Bluetooth adapters to read vehicle diagnostics and send custom commands.
## Features
### 🔌 Bluetooth Connection
- Scan and connect to paired ELM327 Bluetooth devices
- Real-time connection status indicator
- Automatic ELM327 initialization
- Connection management
### 🚗 Vehicle Database
- Comprehensive database of vehicles and OBD signals
- Browse signals by vehicle make and model
- Search functionality for quick access
- Signal details including command, unit, frequency, and description
### 🔍 Quick Diagnostic
- Safe, read-only diagnostic test suite
- Common diagnostic parameters:
- Engine RPM
- Vehicle Speed
- Coolant Temperature
- Throttle Position
- Fuel Level
- Intake Air Temperature
- MAF Air Flow Rate
- Engine Load
- Automatic response parsing
- Command history with timestamps
### ⌨️ Custom Commands
- Send custom OBD2 commands
- Command editing before sending
- Response history
- Raw and parsed response display
- Common command shortcuts
- Resend functionality
### ⚠️ Safety Features
- Warning dialogs before sending any command
- Clear responsibility notices
- Distinction between safe and potentially dangerous commands
- User must explicitly accept risks
## Architecture
### Technology Stack
- **Kotlin** - Modern programming language for Android
- **Jetpack Compose** - Modern declarative UI framework
- **Material Design 3** - Latest Material Design implementation
- **Room Database** - SQLite ORM for vehicle data
- **Coroutines & Flow** - Asynchronous programming
- **MVVM Architecture** - Clean architecture pattern
- **Navigation Component** - Type-safe navigation
### Project Structure
```
app/
├── data/
│ ├── bluetooth/
│ │ └── BluetoothService.kt # Bluetooth communication
│ ├── database/
│ │ ├── entities/ # Room entities
│ │ ├── dao/ # Data Access Objects
│ │ ├── dto/ # Data Transfer Objects
│ │ └── ObdDatabase.kt # Room database
│ ├── obd/
│ │ └── ObdCommands.kt # OBD2 command definitions
│ └── repository/
│ └── ObdRepository.kt # Data repository
├── ui/
│ ├── components/
│ │ └── CommonComponents.kt # Reusable UI components
│ ├── navigation/
│ │ └── Screen.kt # Navigation routes
│ ├── screens/
│ │ ├── HomeScreen.kt # Main dashboard
│ │ ├── BluetoothScreen.kt # Device connection
│ │ ├── VehicleSelectionScreen.kt # Vehicle browser
│ │ ├── DiagnosticScreen.kt # Quick diagnostic
│ │ └── CustomCommandScreen.kt # Custom commands
│ ├── theme/ # App theming
│ └── viewmodel/
│ ├── MainViewModel.kt # Main app state
│ └── VehicleViewModel.kt # Vehicle data state
└── MainActivity.kt # App entry point
```
## Database Schema
### Tables
- **brands** - Vehicle manufacturers
- **models** - Vehicle models
- **generations** - Vehicle generations
- **obd_signals** - OBD command signals
- **dtc_codes** - Diagnostic Trouble Codes
### Views
- **vehicles_view** - Vehicles with signal counts
- **generations_view** - Generations with vehicle info
- **signals_view** - Signals with vehicle info
- **database_stats** - Database statistics
## Bluetooth Communication
### ELM327 Protocol
The app communicates with ELM327 adapters using AT commands and OBD2 PIDs:
#### Initialization Commands
- `ATZ` - Reset device
- `ATE0` - Echo off
- `ATL0` - Line feeds off
- `ATS0` - Spaces off
- `ATSP0` - Set protocol to automatic
#### Common OBD2 PIDs
- `010C` - Engine RPM
- `010D` - Vehicle Speed
- `0105` - Coolant Temperature
- `0111` - Throttle Position
- `012F` - Fuel Level
- And many more...
### Response Parsing
The app automatically parses common PID responses into human-readable format with appropriate units.
## Permissions
### Required Permissions
- **BLUETOOTH** (API ≤ 30) - Bluetooth communication
- **BLUETOOTH_ADMIN** (API ≤ 30) - Bluetooth management
- **BLUETOOTH_CONNECT** (API ≥ 31) - Connect to devices
- **BLUETOOTH_SCAN** (API ≥ 31) - Scan for devices
## Setup & Installation
### Prerequisites
- Android Studio Hedgehog or newer
- Android SDK 24+ (Android 7.0+)
- ELM327 Bluetooth adapter
- Vehicle with OBD2 port
### Build Instructions
1. Clone the repository
2. Open project in Android Studio
3. Sync Gradle dependencies
4. Add your vehicle database to `app/src/main/assets/obd_database.db`
5. Build and run on device (Bluetooth requires physical device)
### Database Setup
Place your SQLite database file with the required schema in:
```
app/src/main/assets/obd_database.db
```
## Usage
### Connecting to Vehicle
1. Pair your ELM327 adapter in Android Bluetooth settings
2. Open the app and navigate to "Bluetooth Connection"
3. Select your ELM327 device from paired devices list
4. Wait for connection confirmation
### Running Diagnostics
1. Ensure Bluetooth connection is active
2. Navigate to "Quick Diagnostic"
3. Tap "Run Test" button
4. Review diagnostic results
### Sending Custom Commands
1. Ensure Bluetooth connection is active
2. Navigate to "Custom Commands"
3. Enter OBD2 command (e.g., "010C")
4. Read and accept warning dialog
5. View response
### Browsing Vehicle Database
1. Navigate to "Vehicle Database"
2. Search or browse for your vehicle
3. Select vehicle to view available signals
4. Tap "Send" on any signal to execute (requires connection)
## Safety & Disclaimer
⚠️ **IMPORTANT**: This application allows direct communication with your vehicle's systems.
### Warnings
- Incorrect commands can damage your vehicle
- Some commands may void warranties
- Users are fully responsible for any consequences
- Read-only commands are generally safe
- Write commands require expert knowledge
### Best Practices
- Start with the safe diagnostic test
- Research commands before sending
- Never send commands while driving
- Keep a record of sent commands
- Consult professional if unsure
## Contributing
Contributions are welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Follow Kotlin coding conventions
4. Add documentation for new features
5. Submit a pull request
## License
This project is provided as-is for educational purposes. Use at your own risk.
## Support
For issues, questions, or contributions, please open an issue on the project repository.
## Acknowledgments
- ELM327 protocol documentation
- OBD2 standard specifications
- Android Jetpack Compose team
- Material Design team
---
**Note**: This application is designed for diagnostic and educational purposes. Always prioritize safety and consult professional mechanics for vehicle repairs.

1
app/.gitignore vendido Archivo normal
Ver fichero

@@ -0,0 +1 @@
/build

75
app/build.gradle.kts Archivo normal
Ver fichero

@@ -0,0 +1,75 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
}
android {
namespace = "com.manalejandro.odb2bluetooth"
compileSdk = 34
defaultConfig {
applicationId = "com.manalejandro.odb2bluetooth"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
// Navigation
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
// Room Database
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
// Coroutines
implementation(libs.kotlinx.coroutines.android)
// Permissions
implementation(libs.accompanist.permissions)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
}

21
app/proguard-rules.pro vendido Archivo normal
Ver fichero

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

Ver fichero

@@ -0,0 +1,24 @@
package com.manalejandro.odb2bluetooth
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.manalejandro.odb2bluetooth", appContext.packageName)
}
}

Ver fichero

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Bluetooth permissions for Android 11 and below -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<!-- Bluetooth permissions for Android 12+ -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ODB2Bluetooth">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.ODB2Bluetooth">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Archivo binario no mostrado.

Ver fichero

@@ -0,0 +1,97 @@
package com.manalejandro.odb2bluetooth
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.manalejandro.odb2bluetooth.ui.navigation.Screen
import com.manalejandro.odb2bluetooth.ui.screens.*
import com.manalejandro.odb2bluetooth.ui.theme.ODB2BluetoothTheme
import com.manalejandro.odb2bluetooth.ui.viewmodel.MainViewModel
/**
* Main Activity - Entry point of the OBD2 Bluetooth application
*/
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ODB2BluetoothTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
OBD2App()
}
}
}
}
}
@Composable
fun OBD2App() {
val navController = rememberNavController()
val mainViewModel: MainViewModel = viewModel()
NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(Screen.Home.route) {
HomeScreen(
viewModel = mainViewModel,
onNavigateToBluetooth = {
navController.navigate(Screen.Bluetooth.route)
},
onNavigateToVehicleSelection = {
navController.navigate(Screen.VehicleSelection.route)
},
onNavigateToDiagnostic = {
navController.navigate(Screen.Diagnostic.route)
},
onNavigateToCustomCommand = {
navController.navigate(Screen.CustomCommand.route)
}
)
}
composable(Screen.Bluetooth.route) {
BluetoothScreen(
viewModel = mainViewModel,
onNavigateBack = { navController.popBackStack() }
)
}
composable(Screen.VehicleSelection.route) {
VehicleSelectionScreen(
mainViewModel = mainViewModel,
onNavigateBack = { navController.popBackStack() }
)
}
composable(Screen.Diagnostic.route) {
DiagnosticScreen(
viewModel = mainViewModel,
onNavigateBack = { navController.popBackStack() }
)
}
composable(Screen.CustomCommand.route) {
CustomCommandScreen(
viewModel = mainViewModel,
onNavigateBack = { navController.popBackStack() }
)
}
}
}

Ver fichero

@@ -0,0 +1,202 @@
package com.manalejandro.odb2bluetooth.data.bluetooth
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothSocket
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.util.UUID
/**
* Service to manage Bluetooth connection with ELM327 OBD2 adapter
*/
class BluetoothService {
private var bluetoothSocket: BluetoothSocket? = null
private var inputStream: InputStream? = null
private var outputStream: OutputStream? = null
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
val connectionState: StateFlow<ConnectionState> = _connectionState
private val _lastResponse = MutableStateFlow("")
val lastResponse: StateFlow<String> = _lastResponse
companion object {
private const val TAG = "BluetoothService"
// Standard SPP UUID for Bluetooth Serial
private val SPP_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
}
enum class ConnectionState {
DISCONNECTED,
CONNECTING,
CONNECTED,
ERROR
}
/**
* Get list of paired Bluetooth devices
*/
@SuppressLint("MissingPermission")
fun getPairedDevices(): List<BluetoothDevice> {
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
return bluetoothAdapter?.bondedDevices?.toList() ?: emptyList()
}
/**
* Connect to a Bluetooth device
*/
@SuppressLint("MissingPermission")
suspend fun connect(device: BluetoothDevice): Result<Unit> = withContext(Dispatchers.IO) {
try {
_connectionState.value = ConnectionState.CONNECTING
// Close any existing connection
disconnect()
// Create socket and connect
bluetoothSocket = device.createRfcommSocketToServiceRecord(SPP_UUID)
bluetoothSocket?.connect()
inputStream = bluetoothSocket?.inputStream
outputStream = bluetoothSocket?.outputStream
// Initialize ELM327
initializeElm327()
_connectionState.value = ConnectionState.CONNECTED
Log.d(TAG, "Connected to ${device.name}")
Result.success(Unit)
} catch (e: Exception) {
Log.e(TAG, "Connection error", e)
_connectionState.value = ConnectionState.ERROR
disconnect()
Result.failure(e)
}
}
/**
* Initialize ELM327 with default commands
*/
private suspend fun initializeElm327() {
// Reset device
sendCommand("ATZ")
kotlinx.coroutines.delay(1000)
// Turn off echo
sendCommand("ATE0")
kotlinx.coroutines.delay(100)
// Set line feed off
sendCommand("ATL0")
kotlinx.coroutines.delay(100)
// Set spaces off
sendCommand("ATS0")
kotlinx.coroutines.delay(100)
// Set protocol to automatic
sendCommand("ATSP0")
kotlinx.coroutines.delay(100)
}
/**
* Send a command to the ELM327 device
*/
suspend fun sendCommand(command: String): Result<String> = withContext(Dispatchers.IO) {
if (_connectionState.value != ConnectionState.CONNECTED) {
return@withContext Result.failure(IOException("Not connected"))
}
try {
val commandWithCR = "$command\r"
outputStream?.write(commandWithCR.toByteArray())
outputStream?.flush()
Log.d(TAG, "Sent: $command")
// Read response
val response = readResponse()
_lastResponse.value = response
Log.d(TAG, "Received: $response")
Result.success(response)
} catch (e: Exception) {
Log.e(TAG, "Send command error", e)
Result.failure(e)
}
}
/**
* Read response from ELM327
*/
private fun readResponse(): String {
val buffer = ByteArray(1024)
val response = StringBuilder()
var bytesRead: Int
try {
val startTime = System.currentTimeMillis()
val timeout = 5000 // 5 seconds timeout
while (System.currentTimeMillis() - startTime < timeout) {
if (inputStream?.available() ?: 0 > 0) {
bytesRead = inputStream?.read(buffer) ?: 0
if (bytesRead > 0) {
val chunk = String(buffer, 0, bytesRead)
response.append(chunk)
// Check for prompt character '>'
if (chunk.contains('>')) {
break
}
}
}
Thread.sleep(10)
}
} catch (e: Exception) {
Log.e(TAG, "Read response error", e)
}
return response.toString()
.replace(">", "")
.replace("\r", "")
.replace("\n", "")
.trim()
}
/**
* Disconnect from the Bluetooth device
*/
fun disconnect() {
try {
inputStream?.close()
outputStream?.close()
bluetoothSocket?.close()
} catch (e: Exception) {
Log.e(TAG, "Disconnect error", e)
} finally {
inputStream = null
outputStream = null
bluetoothSocket = null
_connectionState.value = ConnectionState.DISCONNECTED
}
}
/**
* Check if Bluetooth is available and enabled
*/
fun isBluetoothAvailable(): Boolean {
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
return bluetoothAdapter != null && bluetoothAdapter.isEnabled
}
}

Ver fichero

@@ -0,0 +1,56 @@
package com.manalejandro.odb2bluetooth.data.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.manalejandro.odb2bluetooth.data.database.dao.DtcCodeDao
import com.manalejandro.odb2bluetooth.data.database.dao.ObdSignalDao
import com.manalejandro.odb2bluetooth.data.database.dao.VehicleDao
import com.manalejandro.odb2bluetooth.data.database.entities.Brand
import com.manalejandro.odb2bluetooth.data.database.entities.DtcCode
import com.manalejandro.odb2bluetooth.data.database.entities.Generation
import com.manalejandro.odb2bluetooth.data.database.entities.Model
import com.manalejandro.odb2bluetooth.data.database.entities.ObdSignal
/**
* Main Room database for the OBD2 application
*/
@Database(
entities = [
Brand::class,
Model::class,
Generation::class,
ObdSignal::class,
DtcCode::class
],
version = 1,
exportSchema = false
)
abstract class ObdDatabase : RoomDatabase() {
abstract fun vehicleDao(): VehicleDao
abstract fun obdSignalDao(): ObdSignalDao
abstract fun dtcCodeDao(): DtcCodeDao
companion object {
@Volatile
private var INSTANCE: ObdDatabase? = null
fun getDatabase(context: Context): ObdDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ObdDatabase::class.java,
"obd_database"
)
.createFromAsset("obd_database.db")
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}

Ver fichero

@@ -0,0 +1,28 @@
package com.manalejandro.odb2bluetooth.data.database.dao
import androidx.room.Dao
import androidx.room.Query
import com.manalejandro.odb2bluetooth.data.database.entities.DtcCode
import kotlinx.coroutines.flow.Flow
/**
* Data Access Object for DTC (Diagnostic Trouble Codes) queries
*/
@Dao
interface DtcCodeDao {
@Query("SELECT * FROM dtc_codes ORDER BY code")
fun getAllDtcCodes(): Flow<List<DtcCode>>
@Query("SELECT * FROM dtc_codes WHERE code = :code")
suspend fun getDtcCodeByCode(code: String): DtcCode?
@Query("""
SELECT * FROM dtc_codes
WHERE code LIKE '%' || :query || '%'
OR description LIKE '%' || :query || '%'
ORDER BY code
""")
fun searchDtcCodes(query: String): Flow<List<DtcCode>>
}

Ver fichero

@@ -0,0 +1,50 @@
package com.manalejandro.odb2bluetooth.data.database.dao
import androidx.room.Dao
import androidx.room.Query
import com.manalejandro.odb2bluetooth.data.database.dto.SignalDto
import com.manalejandro.odb2bluetooth.data.database.entities.ObdSignal
import kotlinx.coroutines.flow.Flow
/**
* Data Access Object for OBD signals queries
*/
@Dao
interface ObdSignalDao {
@Query("SELECT * FROM obd_signals WHERE model_id = :modelId ORDER BY signal_id")
fun getSignalsByModel(modelId: Int): Flow<List<ObdSignal>>
@Query("""
SELECT
s.id,
b.name as brand,
m.name as model,
s.signal_id as signalId,
s.name as signalName,
s.path,
s.unit,
s.header,
s.command,
s.frequency,
s.description
FROM obd_signals s
JOIN models m ON s.model_id = m.id
JOIN brands b ON m.brand_id = b.id
WHERE s.model_id = :modelId
ORDER BY s.signal_id
""")
fun getSignalsDtoByModel(modelId: Int): Flow<List<SignalDto>>
@Query("SELECT * FROM obd_signals WHERE id = :signalId")
suspend fun getSignalById(signalId: Int): ObdSignal?
@Query("""
SELECT * FROM obd_signals
WHERE model_id = :modelId
AND (name LIKE '%' || :query || '%' OR signal_id LIKE '%' || :query || '%')
ORDER BY signal_id
""")
fun searchSignals(modelId: Int, query: String): Flow<List<ObdSignal>>
}

Ver fichero

@@ -0,0 +1,59 @@
package com.manalejandro.odb2bluetooth.data.database.dao
import androidx.room.Dao
import androidx.room.Query
import com.manalejandro.odb2bluetooth.data.database.dto.VehicleDto
import com.manalejandro.odb2bluetooth.data.database.entities.Brand
import com.manalejandro.odb2bluetooth.data.database.entities.Model
import kotlinx.coroutines.flow.Flow
/**
* Data Access Object for vehicle-related queries (brands and models)
*/
@Dao
interface VehicleDao {
@Query("SELECT * FROM brands ORDER BY name")
fun getAllBrands(): Flow<List<Brand>>
@Query("SELECT * FROM models WHERE brand_id = :brandId ORDER BY name")
fun getModelsByBrand(brandId: Int): Flow<List<Model>>
@Query("""
SELECT
m.id as modelId,
b.name as brand,
m.name as model,
m.full_name as fullName,
COUNT(DISTINCT g.id) as generationCount,
COUNT(DISTINCT s.id) as signalCount
FROM models m
JOIN brands b ON m.brand_id = b.id
LEFT JOIN generations g ON m.id = g.model_id
LEFT JOIN obd_signals s ON m.id = s.model_id
GROUP BY m.id, b.name, m.name, m.full_name
ORDER BY b.name, m.name
""")
fun getAllVehicles(): Flow<List<VehicleDto>>
@Query("""
SELECT
m.id as modelId,
b.name as brand,
m.name as model,
m.full_name as fullName,
COUNT(DISTINCT g.id) as generationCount,
COUNT(DISTINCT s.id) as signalCount
FROM models m
JOIN brands b ON m.brand_id = b.id
LEFT JOIN generations g ON m.id = g.model_id
LEFT JOIN obd_signals s ON m.id = s.model_id
WHERE b.name LIKE '%' || :query || '%'
OR m.name LIKE '%' || :query || '%'
OR m.full_name LIKE '%' || :query || '%'
GROUP BY m.id, b.name, m.name, m.full_name
ORDER BY b.name, m.name
""")
fun searchVehicles(query: String): Flow<List<VehicleDto>>
}

Ver fichero

@@ -0,0 +1,19 @@
package com.manalejandro.odb2bluetooth.data.database.dto
/**
* Data Transfer Object for signal information including vehicle details
*/
data class SignalDto(
val id: Int,
val brand: String,
val model: String,
val signalId: String,
val signalName: String,
val path: String?,
val unit: String?,
val header: String?,
val command: String?,
val frequency: Double?,
val description: String?
)

Ver fichero

@@ -0,0 +1,14 @@
package com.manalejandro.odb2bluetooth.data.database.dto
/**
* Data Transfer Object for vehicle information combining brand and model data
*/
data class VehicleDto(
val modelId: Int,
val brand: String,
val model: String,
val fullName: String,
val generationCount: Int,
val signalCount: Int
)

Ver fichero

@@ -0,0 +1,15 @@
package com.manalejandro.odb2bluetooth.data.database.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* Entity representing a vehicle brand in the database
*/
@Entity(tableName = "brands")
data class Brand(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String
)

Ver fichero

@@ -0,0 +1,15 @@
package com.manalejandro.odb2bluetooth.data.database.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* Entity representing a diagnostic trouble code (DTC) in the database
*/
@Entity(tableName = "dtc_codes")
data class DtcCode(
@PrimaryKey
val code: String,
val description: String
)

Ver fichero

@@ -0,0 +1,37 @@
package com.manalejandro.odb2bluetooth.data.database.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
/**
* Entity representing a vehicle generation in the database
*/
@Entity(
tableName = "generations",
foreignKeys = [
ForeignKey(
entity = Model::class,
parentColumns = ["id"],
childColumns = ["model_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index("model_id")]
)
data class Generation(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "model_id")
val modelId: Int,
val name: String,
val code: String?,
@ColumnInfo(name = "start_year")
val startYear: Int?,
@ColumnInfo(name = "end_year")
val endYear: Int?,
val description: String?
)

Ver fichero

@@ -0,0 +1,33 @@
package com.manalejandro.odb2bluetooth.data.database.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
/**
* Entity representing a vehicle model in the database
*/
@Entity(
tableName = "models",
foreignKeys = [
ForeignKey(
entity = Brand::class,
parentColumns = ["id"],
childColumns = ["brand_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index("brand_id")]
)
data class Model(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "brand_id")
val brandId: Int,
val name: String,
@ColumnInfo(name = "full_name")
val fullName: String
)

Ver fichero

@@ -0,0 +1,43 @@
package com.manalejandro.odb2bluetooth.data.database.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
/**
* Entity representing an OBD signal (PID command) in the database
*/
@Entity(
tableName = "obd_signals",
foreignKeys = [
ForeignKey(
entity = Model::class,
parentColumns = ["id"],
childColumns = ["model_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index("model_id")]
)
data class ObdSignal(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "model_id")
val modelId: Int,
@ColumnInfo(name = "signal_id")
val signalId: String,
val name: String,
val path: String?,
val unit: String?,
val description: String?,
val header: String?,
val command: String?,
val frequency: Double?,
@ColumnInfo(name = "min_value")
val minValue: Double?,
@ColumnInfo(name = "max_value")
val maxValue: Double?
)

Ver fichero

@@ -0,0 +1,160 @@
package com.manalejandro.odb2bluetooth.data.obd
/**
* Generic OBD2 commands for diagnostics
*/
object ObdCommands {
/**
* Generic safe diagnostic commands
*/
val SAFE_DIAGNOSTIC_COMMANDS = listOf(
ObdCommand(
name = "Vehicle Speed",
command = "010D",
description = "Current vehicle speed in km/h",
unit = "km/h",
isSafe = true
),
ObdCommand(
name = "Engine RPM",
command = "010C",
description = "Engine revolutions per minute",
unit = "RPM",
isSafe = true
),
ObdCommand(
name = "Coolant Temperature",
command = "0105",
description = "Engine coolant temperature",
unit = "°C",
isSafe = true
),
ObdCommand(
name = "Throttle Position",
command = "0111",
description = "Throttle position percentage",
unit = "%",
isSafe = true
),
ObdCommand(
name = "Fuel Level",
command = "012F",
description = "Fuel tank level input",
unit = "%",
isSafe = true
),
ObdCommand(
name = "Intake Air Temperature",
command = "010F",
description = "Intake air temperature",
unit = "°C",
isSafe = true
),
ObdCommand(
name = "MAF Air Flow Rate",
command = "0110",
description = "Mass air flow sensor air flow rate",
unit = "g/s",
isSafe = true
),
ObdCommand(
name = "Engine Load",
command = "0104",
description = "Calculated engine load value",
unit = "%",
isSafe = true
)
)
/**
* DTC (Diagnostic Trouble Codes) commands
*/
val DTC_COMMANDS = listOf(
ObdCommand(
name = "Read DTCs",
command = "03",
description = "Request diagnostic trouble codes",
unit = "",
isSafe = true
),
ObdCommand(
name = "Clear DTCs",
command = "04",
description = "Clear diagnostic trouble codes and stored values",
unit = "",
isSafe = false
),
ObdCommand(
name = "Pending DTCs",
command = "07",
description = "Request pending diagnostic trouble codes",
unit = "",
isSafe = true
)
)
/**
* Parse OBD2 response for common PIDs
*/
fun parseResponse(command: String, response: String): String? {
if (response.contains("NO DATA") || response.contains("ERROR")) {
return null
}
// Remove spaces and common prefixes
val cleanResponse = response.replace(" ", "").replace("41", "").replace("43", "")
return when (command) {
"010C" -> { // RPM
try {
val value = cleanResponse.substring(2, 6).toInt(16)
"${value / 4} RPM"
} catch (e: Exception) { null }
}
"010D" -> { // Speed
try {
val value = cleanResponse.substring(2, 4).toInt(16)
"$value km/h"
} catch (e: Exception) { null }
}
"0105" -> { // Coolant temp
try {
val value = cleanResponse.substring(2, 4).toInt(16) - 40
"$value °C"
} catch (e: Exception) { null }
}
"010F" -> { // Intake air temp
try {
val value = cleanResponse.substring(2, 4).toInt(16) - 40
"$value °C"
} catch (e: Exception) { null }
}
"0111", "0104", "012F" -> { // Percentage values
try {
val value = (cleanResponse.substring(2, 4).toInt(16) * 100) / 255
"$value %"
} catch (e: Exception) { null }
}
"0110" -> { // MAF
try {
val value = cleanResponse.substring(2, 6).toInt(16) / 100.0
String.format("%.2f g/s", value)
} catch (e: Exception) { null }
}
else -> cleanResponse
}
}
}
/**
* Data class representing an OBD command
*/
data class ObdCommand(
val name: String,
val command: String,
val description: String,
val unit: String,
val isSafe: Boolean
)

Ver fichero

@@ -0,0 +1,48 @@
package com.manalejandro.odb2bluetooth.data.repository
import com.manalejandro.odb2bluetooth.data.database.dao.DtcCodeDao
import com.manalejandro.odb2bluetooth.data.database.dao.ObdSignalDao
import com.manalejandro.odb2bluetooth.data.database.dao.VehicleDao
import com.manalejandro.odb2bluetooth.data.database.dto.SignalDto
import com.manalejandro.odb2bluetooth.data.database.dto.VehicleDto
import com.manalejandro.odb2bluetooth.data.database.entities.Brand
import com.manalejandro.odb2bluetooth.data.database.entities.DtcCode
import com.manalejandro.odb2bluetooth.data.database.entities.ObdSignal
import kotlinx.coroutines.flow.Flow
/**
* Repository for OBD database operations
*/
class ObdRepository(
private val vehicleDao: VehicleDao,
private val obdSignalDao: ObdSignalDao,
private val dtcCodeDao: DtcCodeDao
) {
// Vehicle operations
fun getAllVehicles(): Flow<List<VehicleDto>> = vehicleDao.getAllVehicles()
fun searchVehicles(query: String): Flow<List<VehicleDto>> = vehicleDao.searchVehicles(query)
fun getAllBrands(): Flow<List<Brand>> = vehicleDao.getAllBrands()
// Signal operations
fun getSignalsByModel(modelId: Int): Flow<List<SignalDto>> =
obdSignalDao.getSignalsDtoByModel(modelId)
suspend fun getSignalById(signalId: Int): ObdSignal? =
obdSignalDao.getSignalById(signalId)
fun searchSignals(modelId: Int, query: String): Flow<List<ObdSignal>> =
obdSignalDao.searchSignals(modelId, query)
// DTC operations
fun getAllDtcCodes(): Flow<List<DtcCode>> = dtcCodeDao.getAllDtcCodes()
suspend fun getDtcCodeByCode(code: String): DtcCode? =
dtcCodeDao.getDtcCodeByCode(code)
fun searchDtcCodes(query: String): Flow<List<DtcCode>> =
dtcCodeDao.searchDtcCodes(query)
}

Ver fichero

@@ -0,0 +1,148 @@
package com.manalejandro.odb2bluetooth.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
/**
* Warning dialog to inform users about potential risks when sending OBD commands
*/
@Composable
fun WarningDialog(
onDismiss: () -> Unit,
onAccept: () -> Unit,
title: String = "Warning",
message: String
) {
AlertDialog(
onDismissRequest = onDismiss,
icon = {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = "Warning",
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(48.dp)
)
},
title = {
Text(
text = title,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
},
text = {
Text(
text = message,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Justify
)
},
confirmButton = {
Button(
onClick = onAccept,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) {
Text("I Understand - Continue")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
/**
* Connection status indicator
*/
@Composable
fun ConnectionStatusIndicator(
isConnected: Boolean,
deviceName: String?,
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier,
color = if (isConnected)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.errorContainer,
shape = MaterialTheme.shapes.small
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Status dot
Surface(
modifier = Modifier.size(12.dp),
shape = MaterialTheme.shapes.extraLarge,
color = if (isConnected)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.error
) {}
Text(
text = if (isConnected) {
"Connected${deviceName?.let { " to $it" } ?: ""}"
} else {
"Disconnected"
},
style = MaterialTheme.typography.labelLarge,
color = if (isConnected)
MaterialTheme.colorScheme.onPrimaryContainer
else
MaterialTheme.colorScheme.onErrorContainer
)
}
}
}
/**
* Loading overlay
*/
@Composable
fun LoadingOverlay(
isLoading: Boolean,
message: String = "Loading..."
) {
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Surface(
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surface,
tonalElevation = 8.dp,
shadowElevation = 8.dp
) {
Column(
modifier = Modifier.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
CircularProgressIndicator()
Text(
text = message,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
}

Ver fichero

@@ -0,0 +1,14 @@
package com.manalejandro.odb2bluetooth.ui.navigation
/**
* Navigation routes for the app
*/
sealed class Screen(val route: String) {
object Home : Screen("home")
object Bluetooth : Screen("bluetooth")
object VehicleSelection : Screen("vehicle_selection")
object Diagnostic : Screen("diagnostic")
object CustomCommand : Screen("custom_command")
object About : Screen("about")
}

Ver fichero

@@ -0,0 +1,314 @@
package com.manalejandro.odb2bluetooth.ui.screens
import android.Manifest
import android.bluetooth.BluetoothDevice
import android.os.Build
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.manalejandro.odb2bluetooth.data.bluetooth.BluetoothService
import com.manalejandro.odb2bluetooth.ui.components.ConnectionStatusIndicator
import com.manalejandro.odb2bluetooth.ui.viewmodel.MainViewModel
/**
* Bluetooth screen - Device pairing and connection
*/
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
@Composable
fun BluetoothScreen(
viewModel: MainViewModel,
onNavigateBack: () -> Unit
) {
val connectionState by viewModel.bluetoothService.connectionState.collectAsState()
val pairedDevices by viewModel.pairedDevices.collectAsState()
val selectedDevice by viewModel.selectedDevice.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
val errorMessage by viewModel.errorMessage.collectAsState()
val isConnected = connectionState == BluetoothService.ConnectionState.CONNECTED
// Handle Bluetooth permissions
val bluetoothPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
listOf(
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN
)
} else {
listOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN
)
}
val permissionsState = rememberMultiplePermissionsState(bluetoothPermissions)
LaunchedEffect(Unit) {
if (!permissionsState.allPermissionsGranted) {
permissionsState.launchMultiplePermissionRequest()
} else {
viewModel.loadPairedDevices()
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Bluetooth Connection") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
if (!permissionsState.allPermissionsGranted) {
// Permission request UI
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Default.Phone,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Bluetooth Permission Required",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "This app needs Bluetooth permission to connect to OBD2 devices.",
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = { permissionsState.launchMultiplePermissionRequest() }) {
Text("Grant Permission")
}
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Connection status
ConnectionStatusIndicator(
isConnected = isConnected,
deviceName = selectedDevice?.name,
modifier = Modifier.fillMaxWidth()
)
// Connection controls
if (isConnected) {
ElevatedCard(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "Connected Device",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Text(
text = selectedDevice?.name ?: "Unknown",
style = MaterialTheme.typography.bodyLarge
)
Text(
text = selectedDevice?.address ?: "",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { viewModel.disconnect() },
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
),
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.Close, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Disconnect")
}
}
}
}
// Error message
errorMessage?.let { error ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error
)
Text(
text = error,
color = MaterialTheme.colorScheme.onErrorContainer
)
}
}
}
// Paired devices list
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Paired Devices",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
IconButton(onClick = { viewModel.loadPairedDevices() }) {
Icon(Icons.Default.Refresh, contentDescription = "Refresh")
}
}
if (pairedDevices.isEmpty()) {
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "No paired devices found",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "Pair your ELM327 device in system settings",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(pairedDevices) { device ->
DeviceCard(
device = device,
isConnected = device.address == selectedDevice?.address && isConnected,
isLoading = isLoading && device.address == selectedDevice?.address,
onConnect = { viewModel.connectToDevice(device) }
)
}
}
}
}
}
}
}
}
@Composable
private fun DeviceCard(
device: BluetoothDevice,
isConnected: Boolean,
isLoading: Boolean,
onConnect: () -> Unit
) {
ElevatedCard(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = device.name ?: "Unknown Device",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Text(
text = device.address,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(32.dp)
)
} else if (isConnected) {
Surface(
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.primaryContainer
) {
Text(
text = "Connected",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
} else {
Button(onClick = onConnect) {
Text("Connect")
}
}
}
}
}

Ver fichero

@@ -0,0 +1,358 @@
package com.manalejandro.odb2bluetooth.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import com.manalejandro.odb2bluetooth.ui.components.WarningDialog
import com.manalejandro.odb2bluetooth.ui.viewmodel.CommandResult
import com.manalejandro.odb2bluetooth.ui.viewmodel.MainViewModel
import java.text.SimpleDateFormat
import java.util.*
/**
* Custom command screen - Send custom OBD2 commands
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomCommandScreen(
viewModel: MainViewModel,
onNavigateBack: () -> Unit
) {
var commandText by remember { mutableStateOf("") }
var editedCommand by remember { mutableStateOf("") }
var showWarning by remember { mutableStateOf(false) }
val commandHistory by viewModel.commandHistory.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
if (showWarning) {
WarningDialog(
onDismiss = {
showWarning = false
editedCommand = ""
},
onAccept = {
showWarning = false
viewModel.sendCommand(editedCommand)
commandText = ""
editedCommand = ""
},
title = "⚠️ DANGER - Read Carefully",
message = "YOU ARE ABOUT TO SEND A CUSTOM COMMAND TO YOUR VEHICLE.\n\n" +
"Command: $editedCommand\n\n" +
"⚠️ WARNING:\n" +
"• Sending incorrect commands can DAMAGE your vehicle\n" +
"• You are FULLY RESPONSIBLE for any consequences\n" +
"• This action may VOID your warranty\n" +
"• Only proceed if you KNOW what this command does\n\n" +
"Do you accept ALL RISKS and want to proceed?"
)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Custom Commands") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { viewModel.clearHistory() }) {
Icon(Icons.Default.Delete, contentDescription = "Clear history")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// Warning card
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error
)
Column {
Text(
text = "⚠️ DANGER ZONE",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onErrorContainer
)
Text(
text = "Custom commands can damage your vehicle. Use ONLY if you know what you're doing. YOU are responsible for any consequences.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onErrorContainer
)
}
}
}
// Command input
Column(
modifier = Modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = commandText,
onValueChange = { commandText = it.uppercase() },
label = { Text("OBD2 Command") },
placeholder = { Text("e.g., 010C, ATZ, 0105") },
leadingIcon = {
Icon(Icons.Default.Settings, contentDescription = null)
},
trailingIcon = {
if (commandText.isNotEmpty()) {
IconButton(onClick = { commandText = "" }) {
Icon(Icons.Default.Clear, contentDescription = "Clear")
}
}
},
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Characters,
imeAction = ImeAction.Send
),
keyboardActions = KeyboardActions(
onSend = {
if (commandText.isNotBlank()) {
editedCommand = commandText
showWarning = true
}
}
),
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = {
if (commandText.isNotBlank()) {
editedCommand = commandText
showWarning = true
}
},
modifier = Modifier.fillMaxWidth(),
enabled = commandText.isNotBlank() && !isLoading,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) {
Icon(Icons.Default.Send, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Send Command")
}
}
Spacer(modifier = Modifier.height(16.dp))
// Loading indicator
if (isLoading) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
// Common commands
Text(
text = "Common Commands",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 16.dp)
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
listOf("010C", "010D", "0105", "ATZ").forEach { cmd ->
FilterChip(
selected = false,
onClick = { commandText = cmd },
label = { Text(cmd) }
)
}
}
Divider(modifier = Modifier.padding(vertical = 8.dp))
// Command history
Text(
text = "Response History",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 16.dp)
)
if (commandHistory.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "No commands sent yet",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(commandHistory) { result ->
CommandResultCard(
result = result,
onResend = {
commandText = result.command
}
)
}
}
}
}
}
}
@Composable
private fun CommandResultCard(
result: CommandResult,
onResend: () -> Unit
) {
val dateFormat = remember { SimpleDateFormat("HH:mm:ss", Locale.getDefault()) }
val isError = result.parsedResponse.contains("Error", ignoreCase = true) ||
result.rawResponse.contains("NO DATA", ignoreCase = true) ||
result.rawResponse.contains("ERROR", ignoreCase = true)
ElevatedCard(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (isError) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(20.dp)
)
}
Text(
text = result.command,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
)
}
IconButton(
onClick = onResend,
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = "Resend",
modifier = Modifier.size(20.dp)
)
}
}
Divider()
Text(
text = "Response:",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Surface(
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.surfaceVariant
) {
Text(
text = result.rawResponse.ifBlank { "No response" },
style = MaterialTheme.typography.bodyMedium,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
modifier = Modifier.padding(8.dp)
)
}
if (result.parsedResponse != result.rawResponse && result.parsedResponse.isNotBlank()) {
Text(
text = "Parsed: ${result.parsedResponse}",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = if (isError)
MaterialTheme.colorScheme.error
else
MaterialTheme.colorScheme.primary
)
}
Text(
text = dateFormat.format(Date(result.timestamp)),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}

Ver fichero

@@ -0,0 +1,266 @@
package com.manalejandro.odb2bluetooth.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.manalejandro.odb2bluetooth.data.obd.ObdCommands
import com.manalejandro.odb2bluetooth.ui.components.WarningDialog
import com.manalejandro.odb2bluetooth.ui.viewmodel.CommandResult
import com.manalejandro.odb2bluetooth.ui.viewmodel.MainViewModel
import java.text.SimpleDateFormat
import java.util.*
/**
* Diagnostic screen - Run safe diagnostic tests
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DiagnosticScreen(
viewModel: MainViewModel,
onNavigateBack: () -> Unit
) {
val commandHistory by viewModel.commandHistory.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
var showWarning by remember { mutableStateOf(false) }
if (showWarning) {
WarningDialog(
onDismiss = { showWarning = false },
onAccept = {
showWarning = false
viewModel.runDiagnosticTest()
},
title = "Diagnostic Test",
message = "This will run a series of safe, read-only diagnostic commands to gather information from your vehicle. No modifications will be made to your vehicle's systems.\n\nThe test will read:\n• Engine RPM\n• Vehicle Speed\n• Coolant Temperature\n• Throttle Position\n• Fuel Level\n• And more...\n\nDo you want to continue?"
)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Quick Diagnostic") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { viewModel.clearHistory() }) {
Icon(Icons.Default.Delete, contentDescription = "Clear history")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
},
floatingActionButton = {
if (!isLoading) {
ExtendedFloatingActionButton(
onClick = { showWarning = true },
icon = { Icon(Icons.Default.PlayArrow, contentDescription = null) },
text = { Text("Run Test") }
)
}
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// Info card
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Column {
Text(
text = "Safe Diagnostic Test",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = "This test only reads data from your vehicle and will not make any modifications.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}
// Loading indicator
if (isLoading) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
// Command history
if (commandHistory.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "No diagnostic data yet",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "Tap 'Run Test' to start",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(commandHistory) { result ->
DiagnosticResultCard(result)
}
}
}
}
}
}
@Composable
private fun DiagnosticResultCard(result: CommandResult) {
val dateFormat = remember { SimpleDateFormat("HH:mm:ss", Locale.getDefault()) }
val commandName = ObdCommands.SAFE_DIAGNOSTIC_COMMANDS
.find { it.command == result.command }?.name ?: result.command
val isError = result.parsedResponse.contains("Error", ignoreCase = true) ||
result.parsedResponse.contains("NO DATA", ignoreCase = true)
ElevatedCard(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = commandName,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
if (isError) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(20.dp)
)
} else {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
}
}
Divider()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Command:",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = result.command,
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Medium
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Response:",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = result.parsedResponse,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = if (isError)
MaterialTheme.colorScheme.error
else
MaterialTheme.colorScheme.primary
)
}
if (result.rawResponse.isNotBlank()) {
Text(
text = "Raw: ${result.rawResponse}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
)
}
Text(
text = dateFormat.format(Date(result.timestamp)),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}

Ver fichero

@@ -0,0 +1,231 @@
package com.manalejandro.odb2bluetooth.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.manalejandro.odb2bluetooth.data.bluetooth.BluetoothService
import com.manalejandro.odb2bluetooth.ui.components.ConnectionStatusIndicator
import com.manalejandro.odb2bluetooth.ui.viewmodel.MainViewModel
/**
* Home screen - Main dashboard
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
viewModel: MainViewModel,
onNavigateToBluetooth: () -> Unit,
onNavigateToVehicleSelection: () -> Unit,
onNavigateToDiagnostic: () -> Unit,
onNavigateToCustomCommand: () -> Unit
) {
val connectionState by viewModel.bluetoothService.connectionState.collectAsState()
val selectedDevice by viewModel.selectedDevice.collectAsState()
val isConnected = connectionState == BluetoothService.ConnectionState.CONNECTED
Scaffold(
topBar = {
TopAppBar(
title = { Text("OBD2 Bluetooth") },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Connection status
ConnectionStatusIndicator(
isConnected = isConnected,
deviceName = selectedDevice?.name,
modifier = Modifier.fillMaxWidth()
)
// Welcome card
ElevatedCard(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "Welcome to OBD2 Bluetooth",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
Text(
text = "Connect to your vehicle's ELM327 adapter to read and send OBD2 commands.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Text(
text = "Quick Actions",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 8.dp)
)
// Bluetooth connection button
MenuCard(
title = "Bluetooth Connection",
description = if (isConnected)
"Manage connection"
else
"Connect to OBD2 device",
icon = Icons.Default.Phone,
onClick = onNavigateToBluetooth
)
// Vehicle selection button
MenuCard(
title = "Vehicle Database",
description = "Browse OBD signals by vehicle model",
icon = Icons.Default.Build,
onClick = onNavigateToVehicleSelection,
enabled = isConnected
)
// Diagnostic test button
MenuCard(
title = "Quick Diagnostic",
description = "Run safe diagnostic test",
icon = Icons.Default.Star,
onClick = onNavigateToDiagnostic,
enabled = isConnected
)
// Custom command button
MenuCard(
title = "Custom Commands",
description = "Send custom OBD2 commands",
icon = Icons.Default.Settings,
onClick = onNavigateToCustomCommand,
enabled = isConnected
)
// Safety notice
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error
)
Column {
Text(
text = "Safety Notice",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onErrorContainer
)
Text(
text = "Sending incorrect commands can damage your vehicle. Use at your own risk.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onErrorContainer
)
}
}
}
}
}
}
@Composable
private fun MenuCard(
title: String,
description: String,
icon: androidx.compose.ui.graphics.vector.ImageVector,
onClick: () -> Unit,
enabled: Boolean = true
) {
ElevatedCard(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
enabled = enabled
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = MaterialTheme.shapes.medium,
color = if (enabled)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.size(56.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = if (enabled)
MaterialTheme.colorScheme.onPrimaryContainer
else
MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Icon(
imageVector = Icons.Default.KeyboardArrowRight,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}

Ver fichero

@@ -0,0 +1,414 @@
package com.manalejandro.odb2bluetooth.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.manalejandro.odb2bluetooth.data.database.dto.SignalDto
import com.manalejandro.odb2bluetooth.data.database.dto.VehicleDto
import com.manalejandro.odb2bluetooth.ui.components.WarningDialog
import com.manalejandro.odb2bluetooth.ui.viewmodel.MainViewModel
import com.manalejandro.odb2bluetooth.ui.viewmodel.VehicleViewModel
/**
* Vehicle selection screen - Browse vehicles and their OBD signals
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VehicleSelectionScreen(
mainViewModel: MainViewModel,
onNavigateBack: () -> Unit
) {
val vehicleViewModel: VehicleViewModel = viewModel()
val vehicles by vehicleViewModel.vehicles.collectAsState()
val selectedVehicle by vehicleViewModel.selectedVehicle.collectAsState()
val signals by vehicleViewModel.signals.collectAsState()
val searchQuery by vehicleViewModel.searchQuery.collectAsState()
val isLoading by vehicleViewModel.isLoading.collectAsState()
var searchText by remember { mutableStateOf("") }
var showWarning by remember { mutableStateOf(false) }
var selectedSignal by remember { mutableStateOf<SignalDto?>(null) }
if (showWarning && selectedSignal != null) {
WarningDialog(
onDismiss = {
showWarning = false
selectedSignal = null
},
onAccept = {
showWarning = false
selectedSignal?.command?.let { command ->
mainViewModel.sendCommand(command)
}
selectedSignal = null
},
title = "Send OBD Command",
message = "You are about to send this command:\n\n" +
"Signal: ${selectedSignal?.signalName}\n" +
"Command: ${selectedSignal?.command}\n" +
"Description: ${selectedSignal?.description ?: "N/A"}\n\n" +
"⚠️ Warning: Sending commands can potentially affect your vehicle. " +
"Make sure you understand what this command does.\n\n" +
"Do you want to proceed?"
)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Vehicle Database") },
navigationIcon = {
IconButton(onClick = {
if (selectedVehicle != null) {
vehicleViewModel.clearSelection()
} else {
onNavigateBack()
}
}) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
if (selectedVehicle == null) {
// Vehicle list view
Column(
modifier = Modifier.fillMaxSize()
) {
// Search bar
OutlinedTextField(
value = searchText,
onValueChange = {
searchText = it
vehicleViewModel.searchVehicles(it)
},
label = { Text("Search vehicles") },
placeholder = { Text("Brand, model...") },
leadingIcon = {
Icon(Icons.Default.Search, contentDescription = null)
},
trailingIcon = {
if (searchText.isNotEmpty()) {
IconButton(onClick = {
searchText = ""
vehicleViewModel.searchVehicles("")
}) {
Icon(Icons.Default.Clear, contentDescription = "Clear")
}
}
},
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
if (isLoading) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
if (vehicles.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
imageVector = Icons.Default.Build,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "No vehicles found",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(vehicles) { vehicle ->
VehicleCard(
vehicle = vehicle,
onClick = { vehicleViewModel.selectVehicle(vehicle) }
)
}
}
}
}
} else {
// Signal list view
Column(
modifier = Modifier.fillMaxSize()
) {
// Vehicle info
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = selectedVehicle!!.brand,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary
)
Text(
text = selectedVehicle!!.fullName,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Text(
text = "${selectedVehicle!!.signalCount} OBD signals available",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
if (isLoading) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
if (signals.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "No signals available",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(signals) { signal ->
SignalCard(
signal = signal,
onSend = {
selectedSignal = signal
showWarning = true
}
)
}
}
}
}
}
}
}
}
@Composable
private fun VehicleCard(
vehicle: VehicleDto,
onClick: () -> Unit
) {
ElevatedCard(
onClick = onClick,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.primaryContainer,
modifier = Modifier.size(56.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Icon(
imageVector = Icons.Default.Build,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = vehicle.brand,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary
)
Text(
text = vehicle.model,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Text(
text = "${vehicle.signalCount} signals • ${vehicle.generationCount} generations",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Icon(
imageVector = Icons.Default.KeyboardArrowRight,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@Composable
private fun SignalCard(
signal: SignalDto,
onSend: () -> Unit
) {
ElevatedCard(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = signal.signalName,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Text(
text = signal.signalId,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
)
}
Button(
onClick = onSend,
enabled = signal.command != null
) {
Icon(
imageVector = Icons.Default.Send,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
}
}
if (signal.description != null) {
Text(
text = signal.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Divider()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
signal.command?.let {
InfoChip("Command", it)
}
signal.unit?.let {
InfoChip("Unit", it)
}
signal.frequency?.let {
InfoChip("Freq", "${it}Hz")
}
}
}
}
}
@Composable
private fun InfoChip(label: String, value: String) {
Surface(
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.secondaryContainer
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "$label:",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
Text(
text = value,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
}

Ver fichero

@@ -0,0 +1,19 @@
package com.manalejandro.odb2bluetooth.ui.theme
import androidx.compose.ui.graphics.Color
// Light theme colors - Modern tech/automotive inspired
val Blue80 = Color(0xFF80C7FF)
val BlueGrey80 = Color(0xFFB0C7D6)
val Teal80 = Color(0xFF76D9D9)
val Blue40 = Color(0xFF0066CC)
val BlueGrey40 = Color(0xFF546E7A)
val Teal40 = Color(0xFF00897B)
// Additional custom colors
val Success = Color(0xFF4CAF50)
val Warning = Color(0xFFFF9800)
val Error = Color(0xFFF44336)
val Info = Color(0xFF2196F3)

Ver fichero

@@ -0,0 +1,60 @@
package com.manalejandro.odb2bluetooth.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Blue80,
secondary = BlueGrey80,
tertiary = Teal80,
error = Error
)
private val LightColorScheme = lightColorScheme(
primary = Blue40,
secondary = BlueGrey40,
tertiary = Teal40,
error = Error
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun ODB2BluetoothTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

Ver fichero

@@ -0,0 +1,34 @@
package com.manalejandro.odb2bluetooth.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

Ver fichero

@@ -0,0 +1,168 @@
package com.manalejandro.odb2bluetooth.ui.viewmodel
import android.app.Application
import android.bluetooth.BluetoothDevice
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.manalejandro.odb2bluetooth.data.bluetooth.BluetoothService
import com.manalejandro.odb2bluetooth.data.database.ObdDatabase
import com.manalejandro.odb2bluetooth.data.obd.ObdCommand
import com.manalejandro.odb2bluetooth.data.obd.ObdCommands
import com.manalejandro.odb2bluetooth.data.repository.ObdRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
/**
* Main ViewModel for Bluetooth connection and OBD communication
*/
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val database = ObdDatabase.getDatabase(application)
val repository = ObdRepository(
database.vehicleDao(),
database.obdSignalDao(),
database.dtcCodeDao()
)
val bluetoothService = BluetoothService()
private val _pairedDevices = MutableStateFlow<List<BluetoothDevice>>(emptyList())
val pairedDevices: StateFlow<List<BluetoothDevice>> = _pairedDevices
private val _selectedDevice = MutableStateFlow<BluetoothDevice?>(null)
val selectedDevice: StateFlow<BluetoothDevice?> = _selectedDevice
private val _commandHistory = MutableStateFlow<List<CommandResult>>(emptyList())
val commandHistory: StateFlow<List<CommandResult>> = _commandHistory
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading
private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage: StateFlow<String?> = _errorMessage
/**
* Load paired Bluetooth devices
*/
fun loadPairedDevices() {
try {
_pairedDevices.value = bluetoothService.getPairedDevices()
} catch (e: SecurityException) {
_errorMessage.value = "Bluetooth permission denied"
}
}
/**
* Connect to a Bluetooth device
*/
fun connectToDevice(device: BluetoothDevice) {
viewModelScope.launch {
_isLoading.value = true
_selectedDevice.value = device
bluetoothService.connect(device).fold(
onSuccess = {
_errorMessage.value = null
},
onFailure = { error ->
_errorMessage.value = "Connection failed: ${error.message}"
_selectedDevice.value = null
}
)
_isLoading.value = false
}
}
/**
* Disconnect from current device
*/
fun disconnect() {
bluetoothService.disconnect()
_selectedDevice.value = null
_commandHistory.value = emptyList()
}
/**
* Send a command to the OBD device
*/
fun sendCommand(command: String) {
viewModelScope.launch {
_isLoading.value = true
bluetoothService.sendCommand(command).fold(
onSuccess = { response ->
val parsed = ObdCommands.parseResponse(command, response) ?: response
addCommandToHistory(command, response, parsed)
_errorMessage.value = null
},
onFailure = { error ->
_errorMessage.value = "Command failed: ${error.message}"
addCommandToHistory(command, "", "Error: ${error.message}")
}
)
_isLoading.value = false
}
}
/**
* Run generic safe diagnostic test
*/
fun runDiagnosticTest() {
viewModelScope.launch {
_commandHistory.value = emptyList()
for (cmd in ObdCommands.SAFE_DIAGNOSTIC_COMMANDS) {
sendCommand(cmd.command)
kotlinx.coroutines.delay(500) // Wait between commands
}
}
}
/**
* Clear command history
*/
fun clearHistory() {
_commandHistory.value = emptyList()
}
/**
* Clear error message
*/
fun clearError() {
_errorMessage.value = null
}
private fun addCommandToHistory(command: String, rawResponse: String, parsedResponse: String) {
val history = _commandHistory.value.toMutableList()
history.add(
0, // Add to beginning
CommandResult(
command = command,
rawResponse = rawResponse,
parsedResponse = parsedResponse,
timestamp = System.currentTimeMillis()
)
)
// Keep only last 50 commands
_commandHistory.value = history.take(50)
}
override fun onCleared() {
super.onCleared()
bluetoothService.disconnect()
}
}
/**
* Data class representing a command result
*/
data class CommandResult(
val command: String,
val rawResponse: String,
val parsedResponse: String,
val timestamp: Long
)

Ver fichero

@@ -0,0 +1,109 @@
package com.manalejandro.odb2bluetooth.ui.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.manalejandro.odb2bluetooth.data.database.ObdDatabase
import com.manalejandro.odb2bluetooth.data.database.dto.SignalDto
import com.manalejandro.odb2bluetooth.data.database.dto.VehicleDto
import com.manalejandro.odb2bluetooth.data.repository.ObdRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/**
* ViewModel for vehicle selection and signal browsing
*/
class VehicleViewModel(application: Application) : AndroidViewModel(application) {
private val database = ObdDatabase.getDatabase(application)
private val repository = ObdRepository(
database.vehicleDao(),
database.obdSignalDao(),
database.dtcCodeDao()
)
private val _vehicles = MutableStateFlow<List<VehicleDto>>(emptyList())
val vehicles: StateFlow<List<VehicleDto>> = _vehicles
private val _selectedVehicle = MutableStateFlow<VehicleDto?>(null)
val selectedVehicle: StateFlow<VehicleDto?> = _selectedVehicle
private val _signals = MutableStateFlow<List<SignalDto>>(emptyList())
val signals: StateFlow<List<SignalDto>> = _signals
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading
init {
loadVehicles()
}
/**
* Load all vehicles from database
*/
private fun loadVehicles() {
viewModelScope.launch {
_isLoading.value = true
repository.getAllVehicles().collectLatest { vehicleList ->
_vehicles.value = vehicleList
_isLoading.value = false
}
}
}
/**
* Search vehicles by query
*/
fun searchVehicles(query: String) {
_searchQuery.value = query
viewModelScope.launch {
_isLoading.value = true
if (query.isBlank()) {
repository.getAllVehicles().collectLatest { vehicleList ->
_vehicles.value = vehicleList
_isLoading.value = false
}
} else {
repository.searchVehicles(query).collectLatest { vehicleList ->
_vehicles.value = vehicleList
_isLoading.value = false
}
}
}
}
/**
* Select a vehicle and load its signals
*/
fun selectVehicle(vehicle: VehicleDto) {
_selectedVehicle.value = vehicle
loadSignalsForVehicle(vehicle.modelId)
}
/**
* Clear vehicle selection
*/
fun clearSelection() {
_selectedVehicle.value = null
_signals.value = emptyList()
}
/**
* Load signals for a specific vehicle
*/
private fun loadSignalsForVehicle(modelId: Int) {
viewModelScope.launch {
_isLoading.value = true
repository.getSignalsByModel(modelId).collectLatest { signalList ->
_signals.value = signalList
_isLoading.value = false
}
}
}
}

Ver fichero

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

Ver fichero

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

Ver fichero

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Ver fichero

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 1.4 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 2.8 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 982 B

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 1.7 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 1.9 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 3.8 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 2.8 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 5.8 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 3.8 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 7.6 KiB

Ver fichero

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

Ver fichero

@@ -0,0 +1,4 @@
<resources>
<string name="app_name">OBD2 Bluetooth</string>
</resources>

Ver fichero

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.ODB2Bluetooth" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

Ver fichero

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

Ver fichero

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

Ver fichero

@@ -0,0 +1,17 @@
package com.manalejandro.odb2bluetooth
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

6
build.gradle.kts Archivo normal
Ver fichero

@@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}

23
gradle.properties Archivo normal
Ver fichero

@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

45
gradle/libs.versions.toml Archivo normal
Ver fichero

@@ -0,0 +1,45 @@
[versions]
agp = "8.13.2"
kotlin = "2.0.21"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.09.00"
navigationCompose = "2.7.5"
lifecycleViewmodelCompose = "2.6.1"
roomVersion = "2.6.0"
coroutinesVersion = "1.7.3"
accompanistPermissions = "0.32.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
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-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "roomVersion" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomVersion" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "roomVersion" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutinesVersion" }
accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanistPermissions" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version = "2.0.21-1.0.25" }

BIN
gradle/wrapper/gradle-wrapper.jar vendido Archivo normal

Archivo binario no mostrado.

8
gradle/wrapper/gradle-wrapper.properties vendido Archivo normal
Ver fichero

@@ -0,0 +1,8 @@
#Sun Jan 11 14:15:17 CET 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
gradlew vendido Archivo ejecutable
Ver fichero

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
gradlew.bat vendido Archivo normal
Ver fichero

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

23
settings.gradle.kts Archivo normal
Ver fichero

@@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "ODB2 Bluetooth"
include(":app")