From 4f00f89f7cd04d4cacf38fca41b0356ed404df56 Mon Sep 17 00:00:00 2001 From: tuuhin Date: Thu, 5 Feb 2026 18:53:52 +0530 Subject: [PATCH 1/4] Upgraded project to AGP 9.0 --- app/build.gradle.kts | 1 - build.gradle.kts | 1 - gradle.properties | 13 +++++++++++- gradle/libs.versions.toml | 25 ++++++++++++------------ gradle/wrapper/gradle-wrapper.properties | 2 +- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 146440f..6d27eda 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,7 +3,6 @@ import java.util.Properties plugins { alias(libs.plugins.android.application) - alias(libs.plugins.jetbrainsKotlinAndroid) alias(libs.plugins.google.devtools.ksp) alias(libs.plugins.kotlinx.serialization) alias(libs.plugins.google.protobuf) diff --git a/build.gradle.kts b/build.gradle.kts index f9af221..8d44a6b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +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.jetbrainsKotlinAndroid) apply false alias(libs.plugins.google.devtools.ksp) apply false alias(libs.plugins.google.protobuf) apply false alias(libs.plugins.compose.compiler) apply false diff --git a/gradle.properties b/gradle.properties index 20e2a01..8a0bbe1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,15 @@ 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 \ No newline at end of file +android.nonTransitiveRClass=true +android.defaults.buildfeatures.resvalues=true +android.usesSdkInManifest.disallowed=true +android.sdk.defaultTargetSdkToCompileSdkIfUnset=true +android.enableAppCompileTimeRClass=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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e9df305..89149e3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,27 +1,28 @@ [versions] -agp = "8.13.0" +agp = "9.0.0" compose_destination_animation_version = "1.11.9" compose_destination_core_version = "2.3.0" -coreSplashscreen = "1.0.1" -datastore = "1.1.7" -graphicsShapes = "1.0.1" -kotlin = "2.2.20" +coreSplashscreen = "1.2.0" +datastore = "1.2.0" +graphicsShapes = "1.1.0" +kotlin = "2.3.10" coreKtx = "1.17.0" junit = "4.13.2" junitVersion = "1.3.0" espressoCore = "3.7.0" kotlinxCollectionsImmutable = "0.4.0" kotlinxDatetime = "0.7.1" -kotlinxSerializationJson = "1.9.0" -lifecycleRuntimeKtx = "2.9.4" -activityCompose = "1.11.0" -composeBom = "2025.10.00" +kotlinxSerializationJson = "1.10.0" +lifecycleRuntimeKtx = "2.10.0" +activityCompose = "1.12.3" +composeBom = "2026.01.01" koinBom = "4.1.1" materialIconsExtended = "1.7.8" -ksp_version = "2.2.20-2.0.2" -protobufJavalite = "4.33.0" +ksp_version = "2.3.4" +protobufJavalite = "4.33.5" protobuf-protoc-gen-javalite = "3.0.0" -protobuf_plugin_version = "0.9.5" +protobuf_plugin_version = "0.9.6" +runtime = "1.10.2" [libraries] #core diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 125bd62..bd2a56a 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-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 5f26242c1e61755828da616127a3a9da77706752 Mon Sep 17 00:00:00 2001 From: tuuhin Date: Thu, 5 Feb 2026 23:52:04 +0530 Subject: [PATCH 2/4] Included option to update mtu ble BLE device can request a larger transmission unit for conversation, included option for that Alongside BLEConnectionEvents.kt are events for rssi update, phy read , and mtu update no direct read for device rssi BLEPhysicalChannels.kt a subclass for BLE phy read If connection failed gatt is being closed in BLEClientGattCallback.kt --- .../bluetooth_le/AndroidBLEClientConnector.kt | 18 ++++++- .../bluetooth_le/BLEClientGattCallback.kt | 50 ++++++++++++++++--- .../BluetoothLEClientConnector.kt | 5 +- .../bluetooth_le/enums/BLEPhysicalChannels.kt | 7 +++ .../models/BLEConnectionEvents.kt | 10 ++++ .../exceptions/InvalidMTUValueException.kt | 3 ++ .../state/BLEDeviceConfigEvent.kt | 2 + 7 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/enums/BLEPhysicalChannels.kt create mode 100644 app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/models/BLEConnectionEvents.kt create mode 100644 app/src/main/java/com/eva/bluetoothterminalapp/domain/exceptions/InvalidMTUValueException.kt 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 675a403..25e4190 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 @@ -20,6 +20,7 @@ import com.eva.bluetoothterminalapp.domain.bluetooth.models.BluetoothDeviceModel import com.eva.bluetoothterminalapp.domain.bluetooth_le.BluetoothLEClientConnector import com.eva.bluetoothterminalapp.domain.bluetooth_le.enums.BLEConnectionState import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLECharacteristicsModel +import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLEConnectionEvents import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLEDescriptorModel import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLEServiceModel import com.eva.bluetoothterminalapp.domain.exceptions.BLEIndicationOrNotifyRunningException @@ -28,6 +29,7 @@ import com.eva.bluetoothterminalapp.domain.exceptions.BluetoothNotEnabled import com.eva.bluetoothterminalapp.domain.exceptions.BluetoothPermissionNotProvided import com.eva.bluetoothterminalapp.domain.exceptions.InvalidBLEConfigurationException import com.eva.bluetoothterminalapp.domain.exceptions.InvalidDeviceAddressException +import com.eva.bluetoothterminalapp.domain.exceptions.InvalidMTUValueException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -52,8 +54,8 @@ class AndroidBLEClientConnector( override val connectionState: StateFlow get() = _gattCallback.connectionState - override val deviceRssi: StateFlow - get() = _gattCallback.deviceRssi + override val connEvents: Flow + get() = _gattCallback.connEvents override val bleServices: Flow> get() = _gattCallback.bleGattServices @@ -131,6 +133,18 @@ class AndroidBLEClientConnector( } } + override fun onUpdateMTU(mtu: Int): Result { + return try { + if (mtu !in 23..517) return Result.failure(InvalidMTUValueException()) + Log.i(TAG, "REQUESTING MTU UPDATE") + val result = _bLEGatt?.requestMtu(mtu) ?: false + Result.success(result) + } catch (e: Exception) { + Log.d(TAG, "FAILED TO UPDATE MTU", e) + Result.failure(e) + } + } + override fun read(service: BLEServiceModel, characteristic: BLECharacteristicsModel) : Result { return try { 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 a349674..c438df8 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 @@ -1,6 +1,7 @@ package com.eva.bluetoothterminalapp.data.bluetooth_le import android.annotation.SuppressLint +import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCallback import android.bluetooth.BluetoothGattCharacteristic @@ -13,7 +14,9 @@ import com.eva.bluetoothterminalapp.data.mapper.toDomainModelWithName import com.eva.bluetoothterminalapp.data.mapper.toDomainModelWithNames import com.eva.bluetoothterminalapp.data.samples.SampleUUIDReader import com.eva.bluetoothterminalapp.domain.bluetooth_le.enums.BLEConnectionState +import com.eva.bluetoothterminalapp.domain.bluetooth_le.enums.BLEPhysicalChannels import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLECharacteristicsModel +import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLEConnectionEvents import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLEDescriptorModel import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLEServiceModel import kotlinx.collections.immutable.toPersistentList @@ -21,7 +24,10 @@ 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 +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update @@ -38,9 +44,6 @@ class BLEClientGattCallback( private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - private val _deviceRssi = MutableStateFlow(0) - val deviceRssi = _deviceRssi.asStateFlow() - private val _connectionState = MutableStateFlow(BLEConnectionState.CONNECTING) val connectionState = _connectionState.asStateFlow() @@ -54,11 +57,18 @@ class BLEClientGattCallback( private val _readCharacteristic = MutableStateFlow(null) val readCharacteristics = _readCharacteristic.asStateFlow() - override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + private val _events = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val connEvents = _events.asSharedFlow() + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { if (status != BluetoothGatt.GATT_SUCCESS) { _connectionState.update { BLEConnectionState.FAILED } Log.e(GATT_LOGGER, "PROBLEM WITH CONNECTION STATUS CODE: $status") + Log.d(GATT_LOGGER, "CLOSING CONNECTION") + gatt?.close() return } @@ -88,7 +98,26 @@ class BLEClientGattCallback( if (status != BluetoothGatt.GATT_SUCCESS) return Log.d(GATT_LOGGER, "READING RSSI SUCCESSFUL") // update rssi value - _deviceRssi.update { rssi } + _events.tryEmit(BLEConnectionEvents.OnRSSIUpdated(rssi)) + } + + override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) { + if (status != BluetoothGatt.GATT_SUCCESS) return + Log.d(GATT_LOGGER, "UPDATING MTU SUCCESSFUL :$mtu") + _events.tryEmit(BLEConnectionEvents.OnMTUUpdated(mtu)) + } + + override fun onPhyRead(gatt: BluetoothGatt?, txPhy: Int, rxPhy: Int, status: Int) { + if (status != BluetoothGatt.GATT_SUCCESS) return + Log.d(GATT_LOGGER, "PHY READ TX:$txPhy RX:$rxPhy") + } + + override fun onPhyUpdate(gatt: BluetoothGatt?, txPhy: Int, rxPhy: Int, status: Int) { + if (status != BluetoothGatt.GATT_SUCCESS) return + val txChannel = txPhy.toBLPhy() + val rxChannel = rxPhy.toBLPhy() + Log.d(GATT_LOGGER, "PHY READ UPDATED TX:$txChannel RX:$rxChannel") + _events.tryEmit(BLEConnectionEvents.OnPhyUpdated(txChannel, rxChannel)) } override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { @@ -274,7 +303,9 @@ class BLEClientGattCallback( } } - fun cleanUp() = scope.cancel() + fun cleanUp() { + scope.cancel() + } fun findCharacteristicFromDomainModel( service: BLEServiceModel, @@ -297,4 +328,11 @@ class BLEClientGattCallback( ?.getDescriptor(descriptor.uuid) } + + private fun Int.toBLPhy() = when (this) { + BluetoothDevice.PHY_LE_CODED -> BLEPhysicalChannels.LE_CODED + BluetoothDevice.PHY_LE_1M -> BLEPhysicalChannels.LE_1M + BluetoothDevice.PHY_LE_2M -> BLEPhysicalChannels.LE_2M + else -> throw IllegalArgumentException("Invalid transmission value") + } } \ No newline at end of file diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/BluetoothLEClientConnector.kt b/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/BluetoothLEClientConnector.kt index 73c4163..ae36e5e 100644 --- a/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/BluetoothLEClientConnector.kt +++ b/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/BluetoothLEClientConnector.kt @@ -4,6 +4,7 @@ import com.eva.bluetoothterminalapp.domain.bluetooth.models.BluetoothDeviceModel import com.eva.bluetoothterminalapp.domain.bluetooth_le.enums.BLEConnectionState import com.eva.bluetoothterminalapp.domain.bluetooth_le.enums.BLEPropertyTypes import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLECharacteristicsModel +import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLEConnectionEvents import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLEDescriptorModel import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLEServiceModel import kotlinx.coroutines.flow.Flow @@ -20,7 +21,7 @@ interface BluetoothLEClientConnector { /** * Receive signal strength of the device */ - val deviceRssi: StateFlow + val connEvents: Flow /** * Available [BLEServiceModel] with the current selected service @@ -117,6 +118,8 @@ interface BluetoothLEClientConnector { ): Result + fun onUpdateMTU(mtu: Int): Result + /** * Rediscover available services for the device * @return [Result] If the operation done correctly diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/enums/BLEPhysicalChannels.kt b/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/enums/BLEPhysicalChannels.kt new file mode 100644 index 0000000..1aa0cb2 --- /dev/null +++ b/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/enums/BLEPhysicalChannels.kt @@ -0,0 +1,7 @@ +package com.eva.bluetoothterminalapp.domain.bluetooth_le.enums + +enum class BLEPhysicalChannels { + LE_1M, + LE_2M, + LE_CODED, +} \ No newline at end of file diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/models/BLEConnectionEvents.kt b/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/models/BLEConnectionEvents.kt new file mode 100644 index 0000000..bec89b7 --- /dev/null +++ b/app/src/main/java/com/eva/bluetoothterminalapp/domain/bluetooth_le/models/BLEConnectionEvents.kt @@ -0,0 +1,10 @@ +package com.eva.bluetoothterminalapp.domain.bluetooth_le.models + +import com.eva.bluetoothterminalapp.domain.bluetooth_le.enums.BLEPhysicalChannels + +sealed class BLEConnectionEvents { + data class OnRSSIUpdated(val rssi: Int) : BLEConnectionEvents() + data class OnMTUUpdated(val mtu: Int) : BLEConnectionEvents() + data class OnPhyUpdated(val tx: BLEPhysicalChannels, val rx: BLEPhysicalChannels) : + BLEConnectionEvents() +} \ No newline at end of file diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/domain/exceptions/InvalidMTUValueException.kt b/app/src/main/java/com/eva/bluetoothterminalapp/domain/exceptions/InvalidMTUValueException.kt new file mode 100644 index 0000000..ff2bea0 --- /dev/null +++ b/app/src/main/java/com/eva/bluetoothterminalapp/domain/exceptions/InvalidMTUValueException.kt @@ -0,0 +1,3 @@ +package com.eva.bluetoothterminalapp.domain.exceptions + +class InvalidMTUValueException : Exception("Invalid value for mtu cannot be larger than 517") \ No newline at end of file diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/state/BLEDeviceConfigEvent.kt b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/state/BLEDeviceConfigEvent.kt index b34a347..7771283 100644 --- a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/state/BLEDeviceConfigEvent.kt +++ b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/state/BLEDeviceConfigEvent.kt @@ -9,4 +9,6 @@ sealed interface BLEDeviceConfigEvent { data object OnReadRssiStrength : BLEDeviceConfigEvent data object OnReDiscoverServices : BLEDeviceConfigEvent + + data class OnUpdateMTU(val newValue: Int) : BLEDeviceConfigEvent } \ No newline at end of file From ae9b52afbe20de8708acf08c32f0c405d91763f9 Mon Sep 17 00:00:00 2001 From: tuuhin Date: Thu, 5 Feb 2026 23:57:18 +0530 Subject: [PATCH 3/4] Updated shared element modifiers and included mtu dialog Alongside options for refresh services and rssi include option to show dialog for updating mtu In BLEDeviceViewModel.kt option to show toast based on the BLEConnection events, rssi is kept zero and only after update its refreshed --- .../feature_le_connect/BLEDeviceViewModel.kt | 89 ++++++++------ .../composables/BLEDeviceRequestMTUDialog.kt | 112 ++++++++++++++++++ .../composables/BLEDeviceRouteTopBar.kt | 18 +++ .../util/SharedElementModifiers.kt | 55 +++++++-- app/src/main/res/values/strings.xml | 4 + 5 files changed, 232 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/composables/BLEDeviceRequestMTUDialog.kt diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/BLEDeviceViewModel.kt b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/BLEDeviceViewModel.kt index d8452e7..ece9c56 100644 --- a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/BLEDeviceViewModel.kt +++ b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/BLEDeviceViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.eva.bluetoothterminalapp.domain.bluetooth_le.BluetoothLEClientConnector import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLECharacteristicsModel +import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLEConnectionEvents import com.eva.bluetoothterminalapp.domain.bluetooth_le.models.BLEDescriptorModel import com.eva.bluetoothterminalapp.presentation.feature_le_connect.state.BLECharacteristicEvent import com.eva.bluetoothterminalapp.presentation.feature_le_connect.state.BLEDeviceConfigEvent @@ -24,6 +25,10 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -45,22 +50,33 @@ class BLEDeviceViewModel( selectedCharacteristic, bleConnector.isNotifyOrIndicationRunning, transform = ::readCharacteristics - ).onStart { initiateConnection() } + ).onStart { + initiateConnection() + observeBLEEvents() + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(10_000), + initialValue = null + ) + + private val deviceRssi = bleConnector.connEvents + .filterIsInstance() + .map { it.rssi } .stateIn( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(10_000), - initialValue = null + started = SharingStarted.Eagerly, + initialValue = 0 ) val bLEProfile = combine( - bleConnector.deviceRssi, + deviceRssi, bleConnector.bleServices, bleConnector.connectionState, - ) { deviceRssi, services, connectState -> + ) { rssi, services, connectState -> BLEDeviceProfileState( connectionState = connectState, device = bleConnector.connectedDevice, - signalStrength = deviceRssi, + signalStrength = rssi, services = services.toImmutableList() ) }.stateIn( @@ -77,7 +93,7 @@ class BLEDeviceViewModel( override val uiEvents: SharedFlow get() = _uiEvents.asSharedFlow() - private val navArgs: BluetoothDeviceArgs? + private val navArgs: BluetoothDeviceArgs get() = savedStateHandle.navArgs() private val isNotifyOrIndicationRunning: Boolean @@ -125,6 +141,7 @@ class BLEDeviceViewModel( BLEDeviceConfigEvent.OnReDiscoverServices -> onRefreshServices() BLEDeviceConfigEvent.OnDisconnectEvent -> bleConnector.disconnect() BLEDeviceConfigEvent.OnReconnectEvent -> onClientReconnect() + is BLEDeviceConfigEvent.OnUpdateMTU -> onUpdateMTU(event.newValue) } } @@ -171,12 +188,7 @@ class BLEDeviceViewModel( } private fun initiateConnection() { - val address = navArgs?.address ?: return run { - viewModelScope.launch { - val event = UiEvents.ShowSnackBar("Cannot find the given address") - _uiEvents.emit(event) - } - } + val address = navArgs.address // being connection viewModelScope.launch { bleConnector.connect(address) @@ -271,37 +283,42 @@ class BLEDeviceViewModel( private fun onRefreshRSSI() = viewModelScope.launch { val result = bleConnector.checkRssi() - result.fold( - onSuccess = { isOk -> - val message = if (isOk) "Refreshed RSSI" else "Failed to refresh" - _uiEvents.emit(UiEvents.ShowToast(message)) - }, - onFailure = { error -> - val error = error.message ?: "Cannot perform refresh" - _uiEvents.emit(UiEvents.ShowSnackBar(error)) - }, - ) + if (result.isSuccess) return@launch + + val error = result.exceptionOrNull()?.message ?: "Cannot perform refresh" + _uiEvents.emit(UiEvents.ShowSnackBar(error)) + } private fun onRefreshServices() = viewModelScope.launch { val result = bleConnector.discoverServices() - result.fold( - onSuccess = { isOk -> - val message = if (isOk) "Refreshed Services" else "Failed to refresh" - _uiEvents.emit(UiEvents.ShowToast(message)) - }, - onFailure = { error -> - val error = error.message ?: "Cannot perform refresh" - _uiEvents.emit(UiEvents.ShowSnackBar(error)) - }, - ) + if (result.isSuccess) return@launch + + val error = result.exceptionOrNull()?.message ?: "Cannot perform refresh" + _uiEvents.emit(UiEvents.ShowSnackBar(error)) + } + private fun onUpdateMTU(unit: Int) = viewModelScope.launch { + val result = bleConnector.onUpdateMTU(unit) + if (result.isSuccess) return@launch + + val error = result.exceptionOrNull()?.message ?: "Cannot update mtu" + _uiEvents.emit(UiEvents.ShowSnackBar(error)) + } + + private fun observeBLEEvents() { + + bleConnector.connEvents.onEach { event -> + val message = when (event) { + is BLEConnectionEvents.OnMTUUpdated -> "Device MTU updated :${event.mtu}" + is BLEConnectionEvents.OnPhyUpdated -> "Device phy updated" + is BLEConnectionEvents.OnRSSIUpdated -> "Device RSSI updated" + } + _uiEvents.emit(UiEvents.ShowToast(message)) + }.launchIn(viewModelScope) } override fun onCleared() { - // close the ble connection bleConnector.close() - // clear the viewmodel - super.onCleared() } } diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/composables/BLEDeviceRequestMTUDialog.kt b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/composables/BLEDeviceRequestMTUDialog.kt new file mode 100644 index 0000000..cecee11 --- /dev/null +++ b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/composables/BLEDeviceRequestMTUDialog.kt @@ -0,0 +1,112 @@ +package com.eva.bluetoothterminalapp.presentation.feature_le_connect.composables + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import com.eva.bluetoothterminalapp.R +import com.eva.bluetoothterminalapp.ui.theme.BlueToothTerminalAppTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BLEDeviceRequestMTUDialog( + showDialog: Boolean, + onRequestValue: (Int) -> Unit, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier +) { + val focusRequester = remember { FocusRequester() } + val textState = rememberTextFieldState() + + if (!showDialog) return + + AlertDialog( + onDismissRequest = onDismissRequest, + modifier = modifier, + title = { Text(text = stringResource(R.string.ble_device_profile_action_request_mtu)) }, + confirmButton = { + Button( + onClick = { + val paredValue = textState.text.toString().toIntOrNull() + if (paredValue != null && paredValue in 23..517) + onRequestValue(paredValue) + }, + ) { + Text(text = stringResource(R.string.dialog_action_request)) + } + }, + dismissButton = { + TextButton( + onClick = onDismissRequest, + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary) + ) { + Text(text = stringResource(R.string.dialog_action_cancel)) + } + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.wrapContentHeight() + ) { + OutlinedTextField( + state = textState, + trailingIcon = { + IconButton(onClick = { focusRequester.freeFocus() }) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = "Cancel focus" + ) + } + }, + placeholder = { Text(text = stringResource(R.string.ble_device_profile_mtu_placeholder)) }, + shape = MaterialTheme.shapes.large, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + lineLimits = TextFieldLineLimits.SingleLine, + contentPadding = PaddingValues(12.dp), + modifier = Modifier.focusRequester(focusRequester) + ) + Text( + text = stringResource(R.string.ble_device_profile_action_request_mtu_info), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.tertiary, + textAlign = TextAlign.Center + ) + } + } + ) +} + +@PreviewLightDark +@Composable +private fun BLEDeviceRequestMTUDialogPreview() = BlueToothTerminalAppTheme { + BLEDeviceRequestMTUDialog( + showDialog = true, + onDismissRequest = {}, + onRequestValue = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/composables/BLEDeviceRouteTopBar.kt b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/composables/BLEDeviceRouteTopBar.kt index e117ccb..8febeca 100644 --- a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/composables/BLEDeviceRouteTopBar.kt +++ b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/feature_le_connect/composables/BLEDeviceRouteTopBar.kt @@ -48,6 +48,17 @@ fun BLEDeviceRouteTopBar( var isMenuExpanded by remember { mutableStateOf(false) } var menuOffset by remember { mutableStateOf(DpOffset.Zero) } + var showRequestMTUDialog by remember { mutableStateOf(false) } + + BLEDeviceRequestMTUDialog( + showDialog = showRequestMTUDialog, + onDismissRequest = { showRequestMTUDialog = false }, + onRequestValue = { unit -> + onConfigEvent(BLEDeviceConfigEvent.OnUpdateMTU(unit)) + showRequestMTUDialog = false + }, + ) + MediumTopAppBar( title = { Text(text = stringResource(id = R.string.ble_client_screen_title)) }, actions = { @@ -102,6 +113,13 @@ fun BLEDeviceRouteTopBar( colors = MenuDefaults .itemColors(leadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer) ) + DropdownMenuItem( + text = { Text(text = stringResource(R.string.ble_device_profile_action_request_mtu)) }, + enabled = connectionState == BLEConnectionState.CONNECTED, + onClick = { showRequestMTUDialog = true }, + colors = MenuDefaults + .itemColors(leadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer) + ) } } }, diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/util/SharedElementModifiers.kt b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/util/SharedElementModifiers.kt index da0a3dd..2305b7b 100644 --- a/app/src/main/java/com/eva/bluetoothterminalapp/presentation/util/SharedElementModifiers.kt +++ b/app/src/main/java/com/eva/bluetoothterminalapp/presentation/util/SharedElementModifiers.kt @@ -2,26 +2,25 @@ package com.eva.bluetoothterminalapp.presentation.util - import androidx.compose.animation.BoundsTransform import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope -import androidx.compose.animation.SharedTransitionScope.ResizeMode -import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.ScaleToBounds import androidx.compose.animation.core.Spring.StiffnessMediumLow import androidx.compose.animation.core.VisibilityThreshold import androidx.compose.animation.core.spring import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.ContentScale - private val NormalSpring = spring( stiffness = StiffnessMediumLow, visibilityThreshold = Rect.VisibilityThreshold @@ -32,8 +31,9 @@ fun Modifier.sharedElementWrapper( key: Any, renderInOverlayDuringTransition: Boolean = true, zIndexInOverlay: Float = 0f, - placeHolderSize: SharedTransitionScope.PlaceHolderSize = SharedTransitionScope.PlaceHolderSize.contentSize, + placeHolderSize: SharedTransitionScope.PlaceholderSize = SharedTransitionScope.PlaceholderSize.ContentSize, boundsTransform: BoundsTransform = BoundsTransform { _, _ -> NormalSpring }, + clipShape: Shape = RectangleShape, ) = composed { val transitionScope = LocalSharedTransitionScopeProvider.current ?: return@composed Modifier val visibilityScope = @@ -47,8 +47,9 @@ fun Modifier.sharedElementWrapper( animatedVisibilityScope = visibilityScope, renderInOverlayDuringTransition = renderInOverlayDuringTransition, zIndexInOverlay = zIndexInOverlay, - placeHolderSize = placeHolderSize, - boundsTransform = boundsTransform + placeholderSize = placeHolderSize, + boundsTransform = boundsTransform, + clipInOverlayDuringTransition = OverlayClip(clipShape) ) } } @@ -58,10 +59,14 @@ fun Modifier.sharedBoundsWrapper( enter: EnterTransition = fadeIn(), exit: ExitTransition = fadeOut(), renderInOverlayDuringTransition: Boolean = true, - resizeMode: ResizeMode = ScaleToBounds(ContentScale.FillWidth, Center), + resizeMode: SharedTransitionScope.ResizeMode = SharedTransitionScope.ResizeMode.scaleToBounds( + ContentScale.FillWidth, + Center + ), zIndexInOverlay: Float = 0f, - placeHolderSize: SharedTransitionScope.PlaceHolderSize = SharedTransitionScope.PlaceHolderSize.contentSize, + placeHolderSize: SharedTransitionScope.PlaceholderSize = SharedTransitionScope.PlaceholderSize.ContentSize, boundsTransform: BoundsTransform = BoundsTransform { _, _ -> NormalSpring }, + clipShape: Shape = RectangleShape, ) = composed { val transitionScope = LocalSharedTransitionScopeProvider.current ?: return@composed Modifier @@ -79,8 +84,38 @@ fun Modifier.sharedBoundsWrapper( boundsTransform = boundsTransform, renderInOverlayDuringTransition = renderInOverlayDuringTransition, zIndexInOverlay = zIndexInOverlay, - placeHolderSize = placeHolderSize, + placeholderSize = placeHolderSize, resizeMode = resizeMode, + clipInOverlayDuringTransition = OverlayClip(clipShape) ) } +} + +@Composable +fun Modifier.sharedTransitionSkipChildSize(): Modifier { + val transitionScope = LocalSharedTransitionScopeProvider.current ?: return this + + return with(transitionScope) { + this@sharedTransitionSkipChildSize.skipToLookaheadSize() + } +} + +@Composable +fun Modifier.sharedTransitionSkipChildPosition(): Modifier { + val transitionScope = LocalSharedTransitionScopeProvider.current ?: return this + + return with(transitionScope) { + this@sharedTransitionSkipChildPosition + .skipToLookaheadPosition() + } +} + + +@Composable +fun Modifier.sharedTransitionRenderInOverlay(zIndexInOverlay: Float): Modifier { + val transitionScope = LocalSharedTransitionScopeProvider.current ?: return this + return with(transitionScope) { + this@sharedTransitionRenderInOverlay + .renderInSharedTransitionScopeOverlay(zIndexInOverlay = zIndexInOverlay) + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d808161..4da6191 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -221,4 +221,8 @@ Connecting to device No Services found, make sure device is connected Characteristics + Request MTU + Request + On some device updating the mtu will work a singletime with max 517, independent of the value entered + 23-517 \ No newline at end of file From 0a7ebbc7777306b55874ce1694b6629977a70256 Mon Sep 17 00:00:00 2001 From: tuuhin Date: Sat, 7 Feb 2026 20:31:52 +0530 Subject: [PATCH 4/4] updated version in build.gradle.kts and removed deprecated method in LightSensorReaderImpl.kt --- app/build.gradle.kts | 4 ++-- .../data/device/LightSensorReaderImpl.kt | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6d27eda..182c22f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,8 +18,8 @@ android { applicationId = "com.eva.bluetoothterminalapp" minSdk = 29 targetSdk = 36 - versionCode = 4 - versionName = "1.2.0" + versionCode = 5 + versionName = "1.2.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/eva/bluetoothterminalapp/data/device/LightSensorReaderImpl.kt b/app/src/main/java/com/eva/bluetoothterminalapp/data/device/LightSensorReaderImpl.kt index 7752c40..71b6827 100644 --- a/app/src/main/java/com/eva/bluetoothterminalapp/data/device/LightSensorReaderImpl.kt +++ b/app/src/main/java/com/eva/bluetoothterminalapp/data/device/LightSensorReaderImpl.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit @@ -36,11 +37,11 @@ class LightSensorReaderImpl(private val context: Context) : LightSensorReader { @OptIn(ExperimentalCoroutinesApi::class) override suspend fun readCurrentValue(): Float? = suspendCancellableCoroutine { cont -> if (!_isSensorAvailable) { - cont.resume(null, onCancellation = {}) + cont.resume(null) return@suspendCancellableCoroutine } val sensor = _lightSensor ?: run { - cont.resume(null, onCancellation = {}) + cont.resume(null) return@suspendCancellableCoroutine } @@ -49,7 +50,7 @@ class LightSensorReaderImpl(private val context: Context) : LightSensorReader { override fun onSensorChanged(event: SensorEvent?) { val lux = event?.values?.firstOrNull() if (cont.isActive) { - cont.resume(lux, onCancellation = {}) + cont.resume(lux) _sensorManager?.unregisterListener(this) Log.d(TAG, "LIGHT SENSOR LISTENER UN-REGISTERED") } @@ -62,7 +63,7 @@ class LightSensorReaderImpl(private val context: Context) : LightSensorReader { if (!registered) { Log.e(TAG, "CANNOT REGISTER FOR SENSOR") - cont.resume(null, onCancellation = null) + cont.resume(null) return@suspendCancellableCoroutine } Log.d(TAG, "LIGHT SENSOR LISTENER REGISTERED")