diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 146440f..182c22f 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) @@ -19,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/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/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") 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/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/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 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 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