diff --git a/.gitignore b/.gitignore
index 073bf60..01f5381 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@
.cxx
local.properties
keystore.properties
+/.junie
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..472d5ea
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..a55e7a1
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/icon.svg b/.idea/icon.svg
new file mode 100644
index 0000000..7693e10
--- /dev/null
+++ b/.idea/icon.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/.idea/markdown.xml b/.idea/markdown.xml
new file mode 100644
index 0000000..c61ea33
--- /dev/null
+++ b/.idea/markdown.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/studiobot.xml b/.idea/studiobot.xml
index 9298202..539e3b8 100644
--- a/.idea/studiobot.xml
+++ b/.idea/studiobot.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..24969a2
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,104 @@
+# BluetoothTerminalApp
+
+This document provides an immediate, high-level understanding of the BluetoothTerminalApp codebase
+for AI assistants, agents, and new developers.
+
+## 1. Architecture Overview
+
+The project follows **Clean Architecture** principles combined with **MVVM (
+Model-ViewModel-ViewModel)** for the presentation layer. It is a single-module Android application (
+`app` module) with internal layering and feature-based organization.
+
+### Directory Structure
+
+- `app/src/main/java/com/eva/bluetoothterminalapp/`
+ - `data/`: Implementations of domain repositories. Contains Android-specific Bluetooth and BLE
+ logic, DataStore persistence (Protobuf), and mappers.
+ - `domain/`: Business logic abstractions. Contains repository interfaces, domain models, enums,
+ and exceptions. This layer is independent of Android-specific implementation details where
+ possible.
+ - `di/`: Koin dependency injection modules (organized by functionality like `BLEModule`,
+ `BluetoothModule`, etc.).
+ - `presentation/`: UI logic and State management.
+ - Organized by features: `feature_connect`, `feature_devices`, `feature_le_connect`, etc.
+ - Each feature contains its own ViewModels, event contracts, and screen-level composables.
+ - `ui/`: Global UI theme (Color, Typography, Shape).
+
+## 2. Core Tech Stack & Dependencies
+
+- **UI:** Jetpack Compose (Material 3) with **Compose Destinations** for navigation.
+- **Concurrency:** Kotlin Coroutines & Flow for asynchronous operations and reactive state.
+- **Dependency Injection:** **Koin** (using `KoinStartup` for initialization).
+- **Persistence:** **DataStore** with **Protobuf** serialization for app settings.
+- **Bluetooth:** Standard Android Bluetooth APIs for Classic BT and Bluetooth Low Energy (BLE).
+- **Serialization:** Kotlinx Serialization for JSON and Protobuf for DataStore.
+
+## 3. Key Logic Hubs (The "Where to Look" Guide)
+
+If you need to modify or understand core functionality, start here:
+
+### Bluetooth Classic (BT)
+
+- **Scanning:** `data/bluetooth/AndroidBluetoothScanner.kt`
+- **Connection:** `data/bluetooth/AndroidBTClientConnector.kt` and `AndroidBTServerConnector.kt`
+- **Data Transfer:** `data/bluetooth/BluetoothTransferService.kt`
+
+### Bluetooth Low Energy (BLE)
+
+- **Scanning:** `data/bluetooth_le/AndroidBluetoothLEScanner.kt`
+- **Connection & GATT:** `data/bluetooth_le/AndroidBLEClientConnector.kt` and
+ `data/bluetooth_le/BLEClientGattCallback.kt`
+
+### Settings & State
+
+- **Persistence:** `data/datastore/` (Implementation) and `domain/settings/repository/` (
+ Interfaces).
+- **Global Settings ViewModel:** `presentation/feature_settings/AppSettingsViewModel.kt`
+
+### Navigation
+
+- **Graph Definition:** `presentation/navigation/AppNavigation.kt`
+- Uses **Compose Destinations** (look for `@Destination` annotations on composables).
+
+## 4. Data Flow & State Management
+
+The app follows a unidirectional data flow (UDF):
+
+1. **User Action:** UI triggers an `Event` (e.g., `BTSettingsEvent`) in the `ViewModel`.
+2. **ViewModel logic:** The `ViewModel` performs logic or calls a `Repository` method.
+3. **Repository Action:** The `Repository` (implemented in `data/`) interacts with the Bluetooth
+ hardware or `DataStore`.
+4. **State Update:** `DataStore` or Bluetooth status flows back as a `Flow`.
+5. **UI Observation:** The `ViewModel` converts the `Flow` into a `StateFlow` (often using
+ `stateIn`). The Compose UI observes this state and recomposes.
+
+## 5. AI/Agent Context & Guidelines
+
+- **UI Development:** Always use **Jetpack Compose**. Follow the Material 3 design system. Design
+ tokens should be pulled from the `ui/theme` package.
+- **Navigation:** Use **Compose Destinations**. Do not manually manage the NavGraph; use the
+ generated code and annotations.
+- **Dependency Injection:** Use **Koin**. When adding new services or ViewModels, ensure they are
+ registered in the appropriate module in the `di/` package.
+- **Immutability:** State exposed from ViewModels should be immutable. Use
+ `kotlinx-collections-immutable` where appropriate.
+- **Permissions:** Bluetooth operations require runtime permissions. Ensure you check for
+ `android.permission.BLUETOOTH_CONNECT`, `BLUETOOTH_SCAN`, and `ACCESS_FINE_LOCATION` where
+ necessary.
+- **Architecture Integrity:** Do not call Android Bluetooth APIs directly from ViewModels. Always go
+ through a domain-defined interface (Repository) and provide an implementation in the `data/`
+ layer.
+- **Naming Conventions:** Repository implementations should be prefixed with `Android` if they are
+ platform-specific (e.g., `AndroidBluetoothScanner`).
+
+## 6. Performance & Optimization (R8)
+
+- **R8 Full Mode:** The project is configured to use **strict R8 Full Mode** (no compatibility
+ flags). This allows for aggressive constructor and member shrinking.
+- **Rule Discipline:** Avoid adding broad keep rules (e.g., `-keep class com.package.** { *; }`).
+ Prefer narrow rules or relying on library consumer rules.
+- **Protobuf:** Unused field shrinking for Protobuf is enabled via `-shrinkunusedprotofields` in
+ `proguard-rules.pro`.
+- **Validation:** When upgrading libraries or adding reflection-heavy code, always verify
+ functionality in a `release` build variant to ensure R8 hasn't stripped required members.
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 182c22f..c388b85 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -7,19 +7,18 @@ plugins {
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.google.protobuf)
alias(libs.plugins.compose.compiler)
- id("kotlin-parcelize")
}
android {
namespace = "com.eva.bluetoothterminalapp"
- compileSdk = 36
+ compileSdk = libs.versions.android.compilesdk.get().toInt()
defaultConfig {
applicationId = "com.eva.bluetoothterminalapp"
- minSdk = 29
- targetSdk = 36
- versionCode = 5
- versionName = "1.2.1"
+ minSdk = libs.versions.android.minsdk.get().toInt()
+ targetSdk = libs.versions.android.targetsdk.get().toInt()
+ versionCode = 6
+ versionName = "1.2.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -56,10 +55,8 @@ android {
buildTypes {
debug {
- resValue("string", "app_name", "BluetoothTerminalApp (Debug)")
applicationIdSuffix = ".debug"
isMinifyEnabled = false
- isShrinkResources = false
}
release {
@@ -114,7 +111,6 @@ dependencies {
//lifecycle compose runtime
implementation(libs.androidx.lifecycle.runtime.compose)
//navigation
- implementation(libs.compose.destination.animation)
implementation(libs.compose.destination.core)
ksp(libs.compose.destination.ksp)
//kotlinx
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
new file mode 100644
index 0000000..6a4df40
--- /dev/null
+++ b/app/src/debug/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ BluetoothTerminalApp (Debug)
+
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/data/ble_server/BLEServerGattCallback.kt b/app/src/main/java/com/eva/bluetoothterminalapp/data/ble_server/BLEServerGattCallback.kt
index de9b5eb..adcb9a4 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/data/ble_server/BLEServerGattCallback.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/data/ble_server/BLEServerGattCallback.kt
@@ -168,10 +168,12 @@ class BLEServerGattCallback(
val characteristicDeferred = service.characteristics.map { characteristic ->
async { characteristic.toDomainModelWithNames(uuidReader) }
}
- domainService.probableName = serviceName.await()?.name
- domainService.characteristic = characteristicDeferred.awaitAll().toPersistentList()
+ val updatedService = domainService.copy(
+ probableName = serviceName.await()?.name,
+ characteristics = characteristicDeferred.awaitAll().toPersistentList()
+ )
- _services.update { previous -> (previous + domainService).distinctBy { it.serviceId } }
+ _services.update { previous -> (previous + updatedService).distinctBy { it.serviceId } }
}.invokeOnCompletion {
// inform new service is added
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/data/bluetooth_le/AndroidBLEClientConnector.kt b/app/src/main/java/com/eva/bluetoothterminalapp/data/bluetooth_le/AndroidBLEClientConnector.kt
index 25e4190..fb6dd0d 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/data/bluetooth_le/AndroidBLEClientConnector.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/data/bluetooth_le/AndroidBLEClientConnector.kt
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
+import android.bluetooth.BluetoothGattConnectionSettings
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothStatusCodes
@@ -30,6 +31,12 @@ import com.eva.bluetoothterminalapp.domain.exceptions.BluetoothPermissionNotProv
import com.eva.bluetoothterminalapp.domain.exceptions.InvalidBLEConfigurationException
import com.eva.bluetoothterminalapp.domain.exceptions.InvalidDeviceAddressException
import com.eva.bluetoothterminalapp.domain.exceptions.InvalidMTUValueException
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -39,14 +46,17 @@ import kotlinx.coroutines.flow.update
private const val TAG = "BLE_CLIENT_LOGGER"
@SuppressLint("MissingPermission")
-@Suppress("DEPRECATION")
class AndroidBLEClientConnector(
private val context: Context,
private val reader: SampleUUIDReader,
) : BluetoothLEClientConnector {
+ private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+
private val _bluetoothManager by lazy { context.getSystemService() }
- private val _gattCallback by lazy { BLEClientGattCallback(reader = reader, echoWrite = true) }
+ private val _gattCallback by lazy {
+ BLEClientGattCallback(reader = reader, echoWrite = true, scope = scope)
+ }
private val _btAdapter: BluetoothAdapter?
get() = _bluetoothManager?.adapter
@@ -86,19 +96,38 @@ class AndroidBLEClientConnector(
val device = _btAdapter?.getRemoteDevice(address) ?: return Result.success(false)
// update the device
_connectedDevice = device.toDomainModel()
+
// connect to the gatt server
- _bLEGatt = device.connectGatt(
- context,
- autoConnect,
- _gattCallback,
- BluetoothDevice.TRANSPORT_LE
- )
+ _bLEGatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN) {
+ val settings = BluetoothGattConnectionSettings.Builder()
+ .setTransport(BluetoothDevice.TRANSPORT_LE)
+ .setAutoConnectEnabled(autoConnect)
+ .setOpportunisticEnabled(false)
+ .build()
+
+ // we can have a dedicated executor for the connected client
+ val ioDispatcher = Dispatchers.IO
+ .limitedParallelism(2, "bluetooth_connector_dispatcher")
+ .asExecutor()
+
+ device.connectGatt(settings, ioDispatcher, _gattCallback)
+ } else {
+ @Suppress("DEPRECATION")
+ device.connectGatt(
+ context,
+ autoConnect,
+ _gattCallback,
+ BluetoothDevice.TRANSPORT_LE
+ )
+ }
+
Log.d(TAG, "CONNECT GATT")
// load all files
reader.loadFromFiles()
// return success if there is no error
Result.success(true)
} catch (e: Exception) {
+ if (e is CancellationException) throw e
Result.failure(e)
}
}
@@ -185,7 +214,10 @@ class AndroidBLEClientConnector(
)
operation == BluetoothStatusCodes.SUCCESS
} else {
+ @Suppress("DEPRECATION")
gattCharacteristic.value = bytes
+
+ @Suppress("DEPRECATION")
_bLEGatt?.writeCharacteristic(gattCharacteristic) ?: false
}
@@ -320,7 +352,7 @@ class AndroidBLEClientConnector(
try {
_connectedDevice = null
// cancels the scope
- _gattCallback.cleanUp()
+ scope.cancel()
reader.clearCache()
// close the gatt server
_bLEGatt?.close()
@@ -338,7 +370,10 @@ class AndroidBLEClientConnector(
val operation = _bLEGatt?.writeDescriptor(descriptor, bytes)
operation == BluetoothStatusCodes.SUCCESS
} else {
+ @Suppress("DEPRECATION")
descriptor.value = bytes
+
+ @Suppress("DEPRECATION")
_bLEGatt?.writeDescriptor(descriptor) ?: false
}
Result.success(isSuccess)
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/data/bluetooth_le/AndroidBluetoothLEScanner.kt b/app/src/main/java/com/eva/bluetoothterminalapp/data/bluetooth_le/AndroidBluetoothLEScanner.kt
index 951d49b..92db043 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/data/bluetooth_le/AndroidBluetoothLEScanner.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/data/bluetooth_le/AndroidBluetoothLEScanner.kt
@@ -85,23 +85,27 @@ class AndroidBluetoothLEScanner(
if (result?.isConnectable == false) return
// if results has no address skip
val address = result?.device?.address ?: return
- val deviceAddresses = _devices.value.map { it.deviceModel.address }
- // if it's a new device
- if (address !in deviceAddresses) {
+ val currentList = _devices.value
+ // find the idx of the device with address
+ val index = currentList.indexOfFirst { it.deviceModel.address == address }
+ // if not found it's a new device
+ if (index == -1) {
val newDevice = result.toDomainModel()
// add it to devices
_devices.update { devices -> devices + newDevice }
- // then work is done
return
}
- //if the address already present
- val updatedList = _devices.value.map { device ->
- // if address already present update the rssi of the device
- if (device.deviceModel.address == address) device.copy(rssi = result.rssi)
- // else return the normal device
- else device
+ // if the address already present, only update if RSSI changed to avoid redundant StateFlow notifications
+ val existingDevice = currentList.getOrNull(index) ?: return
+ // if the rssi didn't change skip this
+ if (existingDevice.rssi == result.rssi) return
+ // otherwise update the rssi
+ _devices.update { devices ->
+ devices.mapIndexed { idx, device ->
+ if (idx == index) device.copy(rssi = result.rssi)
+ else device
+ }
}
- _devices.update { updatedList }
}
override fun onScanFailed(errorCode: Int) {
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/data/bluetooth_le/BLEClientGattCallback.kt b/app/src/main/java/com/eva/bluetoothterminalapp/data/bluetooth_le/BLEClientGattCallback.kt
index c438df8..4c53f40 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/data/bluetooth_le/BLEClientGattCallback.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/data/bluetooth_le/BLEClientGattCallback.kt
@@ -20,10 +20,9 @@ import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLEConnectionEven
import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLEDescriptorModel
import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLEServiceModel
import kotlinx.collections.immutable.toPersistentList
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -35,15 +34,13 @@ import kotlinx.coroutines.launch
private const val GATT_LOGGER = "BLE_GATT_CALLBACK"
-@Suppress("DEPRECATION")
@SuppressLint("MissingPermission")
class BLEClientGattCallback(
private val reader: SampleUUIDReader,
private val echoWrite: Boolean = true,
+ private val scope: CoroutineScope = CoroutineScope(SupervisorJob())
) : BluetoothGattCallback() {
- private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
-
private val _connectionState = MutableStateFlow(BLEConnectionState.CONNECTING)
val connectionState = _connectionState.asStateFlow()
@@ -139,6 +136,7 @@ class BLEClientGattCallback(
}
@Deprecated("Deprecated in Java")
+ @Suppress("DEPRECATION")
override fun onCharacteristicRead(
gatt: BluetoothGatt?,
characteristic: BluetoothGattCharacteristic?,
@@ -159,7 +157,6 @@ class BLEClientGattCallback(
if (status != BluetoothGatt.GATT_SUCCESS) return
Log.d(GATT_LOGGER, "READ CHARACTERISTICS :${characteristic.uuid}")
-
scope.launch {
try {
// decode the received value and decide it
@@ -173,6 +170,7 @@ class BLEClientGattCallback(
Log.d(GATT_LOGGER, "VALUE ON READ ${domainModel.byteArray}")
} catch (e: Exception) {
+ if (e is CancellationException) throw e
e.printStackTrace()
Log.e(GATT_LOGGER, "EXCEPTION", e)
}
@@ -192,6 +190,7 @@ class BLEClientGattCallback(
}
@Deprecated("Deprecated in Java")
+ @Suppress("DEPRECATION")
override fun onDescriptorRead(
gatt: BluetoothGatt?,
descriptor: BluetoothGattDescriptor?,
@@ -257,6 +256,7 @@ class BLEClientGattCallback(
}
@Deprecated("Deprecated in Java")
+ @Suppress("DEPRECATION")
override fun onCharacteristicChanged(
gatt: BluetoothGatt?,
characteristic: BluetoothGattCharacteristic?
@@ -303,9 +303,6 @@ class BLEClientGattCallback(
}
}
- fun cleanUp() {
- scope.cancel()
- }
fun findCharacteristicFromDomainModel(
service: BLEServiceModel,
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/data/mapper/BLEGattToReadableDomainModels.kt b/app/src/main/java/com/eva/bluetoothterminalapp/data/mapper/BLEGattToReadableDomainModels.kt
index 0f11b17..35ff027 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/data/mapper/BLEGattToReadableDomainModels.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/data/mapper/BLEGattToReadableDomainModels.kt
@@ -25,7 +25,7 @@ suspend fun List.toDomainModelWithNames(
// return completed results
gattService.toDomainModel(
probableName = serviceName?.name,
- characteristic = characteristicDeferred.awaitAll()
+ characteristics = characteristicDeferred.awaitAll()
)
}
}
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/data/mapper/BluetoothGattCharacteristicToModel.kt b/app/src/main/java/com/eva/bluetoothterminalapp/data/mapper/BluetoothGattCharacteristicToModel.kt
index 959a77e..4f5044c 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/data/mapper/BluetoothGattCharacteristicToModel.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/data/mapper/BluetoothGattCharacteristicToModel.kt
@@ -17,10 +17,9 @@ fun BluetoothGattCharacteristic.toDomainModel(probableName: String? = null): BLE
permission = permission,
properties = bleProperties,
writeType = bleWriteType,
- descriptors = descriptors.map(BluetoothGattDescriptor::toModel).toPersistentList()
- ).apply {
- this.probableName = probableName
- }
+ descriptors = descriptors.map(BluetoothGattDescriptor::toModel).toPersistentList(),
+ probableName = probableName
+ )
fun BluetoothGattCharacteristic.toDomainModel(
probableName: String? = null,
@@ -31,10 +30,10 @@ fun BluetoothGattCharacteristic.toDomainModel(
permission = permission,
properties = bleProperties,
writeType = bleWriteType,
- descriptors = descriptors.toPersistentList()
-).apply {
- this.probableName = probableName
-}
+ descriptors = descriptors.toPersistentList(),
+ probableName = probableName
+)
+
/**
* Characteristic contains properties of indicate
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/data/mapper/BluetoothGattServiceToModel.kt b/app/src/main/java/com/eva/bluetoothterminalapp/data/mapper/BluetoothGattServiceToModel.kt
index c6d277d..1f14f59 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/data/mapper/BluetoothGattServiceToModel.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/data/mapper/BluetoothGattServiceToModel.kt
@@ -8,15 +8,15 @@ import kotlinx.collections.immutable.toPersistentList
fun BluetoothGattService.toDomainModel(
probableName: String? = null,
- characteristic: List = emptyList()
+ characteristics: List = emptyList()
): BLEServiceModel = BLEServiceModel(
serviceId = instanceId,
serviceUUID = uuid,
serviceType = bleServiceType,
-).apply {
- this.characteristic = characteristic.toPersistentList()
- this.probableName = probableName
-}
+ characteristics = characteristics.toPersistentList(),
+ probableName = probableName
+)
+
private val BluetoothGattService.bleServiceType: BLEServicesTypes
get() = when (this.type) {
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth/models/BluetoothMessage.kt b/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth/models/BluetoothMessage.kt
index 8935df6..1525dab 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth/models/BluetoothMessage.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth/models/BluetoothMessage.kt
@@ -8,7 +8,5 @@ data class BluetoothMessage(
val message: String,
val type: BluetoothMessageType,
val uuid: Uuid = Uuid.random(),
-) {
- val logTime: Instant
- get() = Clock.System.now()
-}
\ No newline at end of file
+ val logTime: Instant = Clock.System.now(),
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/models/BLECharacteristicsModel.kt b/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/models/BLECharacteristicsModel.kt
index 8b30886..fde3ff6 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/models/BLECharacteristicsModel.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/models/BLECharacteristicsModel.kt
@@ -16,17 +16,10 @@ data class BLECharacteristicsModel(
val writeType: BLEWriteTypes,
val descriptors: ImmutableList = persistentListOf(),
val byteArray: ByteArray = byteArrayOf(),
+ val probableName: String? = null,
private val isSetNotificationActive: Boolean = false,
) : BLEValueModel(byteArray) {
- private var _probableName: String? = null
-
- var probableName: String?
- get() = _probableName
- set(value) {
- _probableName = value
- }
-
val isIndicationRunning: Boolean
get() = BLEPropertyTypes.PROPERTY_INDICATE in properties && isSetNotificationActive
@@ -51,7 +44,6 @@ data class BLECharacteristicsModel(
if (writeType != other.writeType) return false
if (descriptors != other.descriptors) return false
if (!byteArray.contentEquals(other.byteArray)) return false
- if (_probableName != other._probableName) return false
if (isIndicationRunning != other.isIndicationRunning) return false
if (isNotificationRunning != other.isNotificationRunning) return false
if (isIndicateOrNotify != other.isIndicateOrNotify) return false
@@ -69,7 +61,6 @@ data class BLECharacteristicsModel(
result = 31 * result + writeType.hashCode()
result = 31 * result + descriptors.hashCode()
result = 31 * result + byteArray.contentHashCode()
- result = 31 * result + (_probableName?.hashCode() ?: 0)
result = 31 * result + isIndicationRunning.hashCode()
result = 31 * result + isNotificationRunning.hashCode()
result = 31 * result + isIndicateOrNotify.hashCode()
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/models/BLEServiceModel.kt b/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/models/BLEServiceModel.kt
index 9a0d765..6be7d4a 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/models/BLEServiceModel.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/models/BLEServiceModel.kt
@@ -2,31 +2,16 @@ package com.eva.bluetoothterminalapp.domain.bluetooth_le.models
import com.eva.bluetoothterminalapp.domain.bluetooth_le.enums.BLEServicesTypes
import kotlinx.collections.immutable.PersistentList
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.collections.immutable.persistentListOf
import java.util.UUID
data class BLEServiceModel(
val serviceId: Int,
val serviceUUID: UUID,
val serviceType: BLEServicesTypes = BLEServicesTypes.UNKNOWN,
+ val characteristics: PersistentList = persistentListOf(),
+ val probableName: String? = null,
) {
-
- private var _characteristic: List = emptyList()
- private var _probableName: String? = null
-
- var characteristic: PersistentList
- get() = _characteristic.toPersistentList()
- set(value) {
- _characteristic = value
- }
-
- var probableName: String?
- get() = _probableName
- set(value) {
- _probableName = value
- }
-
-
val characteristicsCount: Int
- get() = _characteristic.size
+ get() = characteristics.size
}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_connect/bt_client/BTClientViewModel.kt b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_connect/bt_client/BTClientViewModel.kt
index c71b929..86a6c7f 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_connect/bt_client/BTClientViewModel.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_connect/bt_client/BTClientViewModel.kt
@@ -36,6 +36,7 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import kotlin.uuid.toJavaUuid
private const val LOGGER = "BLUETOOTH_CLIENT_VIEW_MODEL"
@@ -138,7 +139,10 @@ class BTClientViewModel(
}
// create a client job to connect to the client
_connectAsClientJob = viewModelScope.launch {
- val results = connector.connectClient(clientConnect.address, clientConnect.uuid)
+ val results = connector.connectClient(
+ clientConnect.address,
+ clientConnect.uuid.toJavaUuid()
+ )
results.fold(
onSuccess = {},
onFailure = { err ->
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/composables/BLEDeviceServiceCard.kt b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/composables/BLEDeviceServiceCard.kt
index 4273dc9..29da348 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/composables/BLEDeviceServiceCard.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/composables/BLEDeviceServiceCard.kt
@@ -141,7 +141,7 @@ fun BLEDeviceServiceCard(
)
}
}
- bleService.characteristic.forEach { characteristic ->
+ bleService.characteristics.forEach { characteristic ->
SelectableBLECharacteristics(
characteristic = characteristic,
isSelected = characteristic == selectedCharacteristic,
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/navigation/args/BluetoothClientConnectArgs.kt b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/navigation/args/BluetoothClientConnectArgs.kt
index ca49cf9..e837666 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/navigation/args/BluetoothClientConnectArgs.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/navigation/args/BluetoothClientConnectArgs.kt
@@ -1,11 +1,10 @@
package com.eva.bluetoothterminalapp.presentation.navigation.args
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-import java.util.UUID
+import kotlinx.serialization.Serializable
+import kotlin.uuid.Uuid
-@Parcelize
+@Serializable
data class BluetoothClientConnectArgs(
val address: String,
- val uuid: UUID,
-) : Parcelable
+ val uuid: Uuid,
+)
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/navigation/args/BluetoothDeviceArgs.kt b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/navigation/args/BluetoothDeviceArgs.kt
index 8b1496f..6a517b9 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/navigation/args/BluetoothDeviceArgs.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/navigation/args/BluetoothDeviceArgs.kt
@@ -1,10 +1,9 @@
package com.eva.bluetoothterminalapp.presentation.navigation.args
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
-@Parcelize
+@Serializable
data class BluetoothDeviceArgs(
val address: String,
val name: String? = null,
-) : Parcelable
+)
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/navigation/screens/bt_classic/BTDeviceProfileScreen.kt b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/navigation/screens/bt_classic/BTDeviceProfileScreen.kt
index 88e56bd..901b064 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/navigation/screens/bt_classic/BTDeviceProfileScreen.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/navigation/screens/bt_classic/BTDeviceProfileScreen.kt
@@ -25,6 +25,7 @@ import com.ramcosta.composedestinations.generated.destinations.BtProfileDestinat
import com.ramcosta.composedestinations.generated.destinations.ClientRouteDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import org.koin.androidx.compose.koinViewModel
+import kotlin.uuid.toKotlinUuid
@Destination(
route = Routes.CLIENT_PROFILE_ROUTE,
@@ -52,7 +53,10 @@ fun AnimatedVisibilityScope.BTDeviceProfileScreen(
onEvent = viewmodel::onEvent,
onConnect = { uuid ->
navigator.navigate(
- direction = ClientRouteDestination(address = args.address, uuid = uuid),
+ direction = ClientRouteDestination(
+ address = args.address,
+ uuid = uuid.toKotlinUuid()
+ ),
) {
popUpTo(BtProfileDestination) {
inclusive = true
diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/util/PreviewFakes.kt b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/util/PreviewFakes.kt
index 6559442..48663ba 100644
--- a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/util/PreviewFakes.kt
+++ b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/util/PreviewFakes.kt
@@ -105,11 +105,9 @@ object PreviewFakes {
BLEPropertyTypes.PROPERTY_NOTIFY
),
- descriptors = listOf(FAKE_BLE_DESCRIPTOR_MODEL).toPersistentList()
- ).apply {
+ descriptors = listOf(FAKE_BLE_DESCRIPTOR_MODEL).toPersistentList(),
probableName = "Compose"
-
- }
+ )
val FAKE_BLE_CHARACTERISTIC_MODEL_WITH_DATA = BLECharacteristicsModel(
instanceId = 1,
@@ -126,22 +124,19 @@ object PreviewFakes {
FAKE_BLE_DESCRIPTOR_MODEL,
FAKE_BLE_DESCRIPTOR_MODEL_WITH_VALUE
).toPersistentList(),
- byteArray = ANDROID_NAME_AS_BYTEARRAY
- ).apply {
+ byteArray = ANDROID_NAME_AS_BYTEARRAY,
probableName = "Compose"
-
- }
+ )
val FAKE_SERVICE_WITH_CHARACTERISTICS = BLEServiceModel(
serviceId = 1,
serviceUUID = UUID.fromString("10297702-35bd-4fda-a904-1e693390e08a"),
- serviceType = BLEServicesTypes.SECONDARY
- ).apply {
- characteristic = persistentListOf(
+ serviceType = BLEServicesTypes.SECONDARY,
+ characteristics = persistentListOf(
FAKE_BLE_CHARACTERISTIC_MODEL,
FAKE_BLE_CHARACTERISTIC_MODEL_WITH_DATA
)
- }
+ )
val FAKE_UUID_LIST = List(10) {
UUID.fromString("10297702-35bd-4fda-a904-1e693390e08a")
diff --git a/gradle.properties b/gradle.properties
index 8a0bbe1..4596e01 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,34 +1,19 @@
-# 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
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
+kotlin.daemon.jvmargs=-Xmx3072M
+#Gradle
+org.gradle.jvmargs=-Xmx3072M -Dfile.encoding=UTF-8
+org.gradle.configuration-cache=true
+org.gradle.caching=true
+#Android
android.nonTransitiveRClass=true
-android.defaults.buildfeatures.resvalues=true
-android.usesSdkInManifest.disallowed=true
+android.useAndroidX=true
+android.defaults.buildfeatures.resvalues=false
android.sdk.defaultTargetSdkToCompileSdkIfUnset=true
android.enableAppCompileTimeRClass=true
+android.usesSdkInManifest.disallowed=true
android.uniquePackageNames=false
android.dependency.useConstraints=true
-android.r8.strictFullModeForKeepRules=true
-android.r8.optimizedResourceShrinking=true
-android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false
android.builtInKotlin=true
-android.newDsl=false
\ No newline at end of file
+android.dependency.excludeLibraryComponentsFromConstraints=true
+android.newDsl=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 89149e3..5bbe301 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,28 +1,29 @@
[versions]
-agp = "9.0.0"
-compose_destination_animation_version = "1.11.9"
+agp = "9.2.0"
+android-minsdk = "29"
+android-compilesdk = "37"
+android-targetsdk = "37"
compose_destination_core_version = "2.3.0"
coreSplashscreen = "1.2.0"
-datastore = "1.2.0"
+datastore = "1.2.1"
graphicsShapes = "1.1.0"
-kotlin = "2.3.10"
-coreKtx = "1.17.0"
+kotlin = "2.3.21"
+coreKtx = "1.18.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
kotlinxCollectionsImmutable = "0.4.0"
-kotlinxDatetime = "0.7.1"
-kotlinxSerializationJson = "1.10.0"
+kotlinxDatetime = "0.8.0"
+kotlinxSerializationJson = "1.11.0"
lifecycleRuntimeKtx = "2.10.0"
-activityCompose = "1.12.3"
-composeBom = "2026.01.01"
-koinBom = "4.1.1"
+activityCompose = "1.13.0"
+composeBom = "2026.05.00"
+koinBom = "4.2.1"
materialIconsExtended = "1.7.8"
-ksp_version = "2.3.4"
-protobufJavalite = "4.33.5"
+ksp_version = "2.3.8"
+protobufJavalite = "4.34.1"
protobuf-protoc-gen-javalite = "3.0.0"
-protobuf_plugin_version = "0.9.6"
-runtime = "1.10.2"
+protobuf_plugin_version = "0.10.0"
[libraries]
#core
@@ -48,7 +49,6 @@ androidx-material-icons-extended = { module = "androidx.compose.material:materia
#kotlinx immutable
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
#navigation
-compose_destination_animation = { module = "io.github.raamcosta.compose-destinations:animations-core", version.ref = "compose_destination_animation_version" }
compose_destination_core = { module = "io.github.raamcosta.compose-destinations:core", version.ref = "compose_destination_core_version" }
compose_destination_ksp = { module = "io.github.raamcosta.compose-destinations:ksp", version.ref = "compose_destination_core_version" }
#koin
@@ -74,11 +74,11 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
-jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
google_devtools_ksp = { id = "com.google.devtools.ksp", version.ref = "ksp_version" }
google_protobuf = { id = "com.google.protobuf", version.ref = "protobuf_plugin_version" }
kotlinx_serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize:org.jetbrains.kotlin.plugin.parcelize.gradle.plugin", version.ref = "kotlin" }
[bundles]
compose = ["androidx-ui", "androidx-ui-graphics", "androidx-ui-tooling-preview", "androidx-material3", "androidx-material-icons-extended"]
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index bd2a56a..0d07337 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Sun Mar 31 20:03:44 IST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/stability_config.conf b/stability_config.conf
index ce8c396..8ea150d 100644
--- a/stability_config.conf
+++ b/stability_config.conf
@@ -1,6 +1,7 @@
// consider UUID to be stable
java.util.UUID
// instants should be stable
-kotlinx.datetime.Instant
+kotlin.time.Instant
// specific ble model classes should be stable
+com.eva.bluetoothterminalapp.domain.bluetooth.models.*
com.eva.bluetoothterminalapp.domain.bluetooth_le.models.*
\ No newline at end of file