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 @@ + + + + + + + + + \ 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