From c21b49d7a62adcd1e3ca683b817a54a29b3f1a94 Mon Sep 17 00:00:00 2001 From: semper-viventem Date: Tue, 1 Apr 2025 17:01:38 -0700 Subject: [PATCH 1/3] Improved deep analysis --- app/build.gradle.kts | 4 +- .../software/data/helpers/BleScannerHelper.kt | 70 ++++++++++++++++--- .../DeviceServicesFetchingPlanner.kt | 42 +++++++---- .../interactor/FetchDeviceServiceInfo.kt | 1 + 4 files changed, 95 insertions(+), 22 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 83b11e1..2e4644b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,8 +26,8 @@ android { minSdk = 29 targetSdk = 35 - versionCode = 1708536372 - versionName = "0.29.2-beta" + versionCode = 1708536373 + versionName = "0.29.3-beta" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/f/cking/software/data/helpers/BleScannerHelper.kt b/app/src/main/java/f/cking/software/data/helpers/BleScannerHelper.kt index 1c24945..f1b06b7 100644 --- a/app/src/main/java/f/cking/software/data/helpers/BleScannerHelper.kt +++ b/app/src/main/java/f/cking/software/data/helpers/BleScannerHelper.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import timber.log.Timber import java.util.UUID @@ -147,11 +148,11 @@ class BleScannerHelper( BluetoothProfile.STATE_DISCONNECTING -> { Timber.tag(TAG_CONNECT).d("Disconnecting from device $address") trySend(DeviceConnectResult.Disconnecting) - gatt.close() } BluetoothProfile.STATE_DISCONNECTED -> { Timber.tag(TAG_CONNECT).d("Disconnected from device $address") handleDisconnect(status, gatt) + close(gatt) } else -> { Timber.tag(TAG_CONNECT).e("Error while connecting to device $address. Error code: $status") @@ -198,10 +199,10 @@ class BleScannerHelper( awaitClose { Timber.tag(TAG_CONNECT).d("Closing connection to device $address") - if (requireBluetoothManager().getConnectionState(device, BluetoothProfile.GATT) != BluetoothProfile.STATE_DISCONNECTED) { + if (isDeviceConnected(device)) { gatt.disconnect() } else { - gatt.close() + close(gatt) } } } @@ -220,19 +221,72 @@ class BleScannerHelper( } @SuppressLint("MissingPermission") - fun close(gatt: BluetoothGatt) { - Timber.tag(TAG_CONNECT).d("Closing connection to device ${gatt.device.address}") + fun close(gatt: BluetoothGatt, tag: String = TAG_CONNECT) { + Timber.tag(tag).i("Closing connection to device ${gatt.device.address}") + if (isDeviceConnected(gatt.device)) { + Timber.tag(tag).e("Trying to close connection for device ${gatt.device.address} while it is still connected.") + } gatt.close() + connections.remove(gatt.device.address) } fun closeDeviceConnection(address: String) { connections[address]?.let(::close) - connections.remove(address) } - fun closeAllConnections() { - connections.values.forEach(::close) + @SuppressLint("MissingPermission") + fun isDeviceConnected(device: BluetoothDevice): Boolean { + return requireBluetoothManager().getConnectionState(device, BluetoothProfile.GATT) == BluetoothProfile.STATE_CONNECTED + } + + @SuppressLint("MissingPermission") + fun isDeviceDisconnected(device: BluetoothDevice): Boolean { + return requireBluetoothManager().getConnectionState(device, BluetoothProfile.GATT) == BluetoothProfile.STATE_DISCONNECTED + } + + @SuppressLint("MissingPermission") + suspend fun hardDisconnectDevice(device: BluetoothDevice, tag: String = TAG_CONNECT) { + return callbackFlow { + Timber.tag(tag).i("Trying to close connection to device ${device.address}") + val gatt = device.connectGatt(appContext, false, object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + Timber.tag(tag).i("Connection state change for device ${gatt.device.address}. Status: $status, newState: $newState") + when (newState) { + BluetoothProfile.STATE_CONNECTED -> { + Timber.tag(tag).i("Try disconnect from ${gatt.device.address}") + gatt.disconnect() + } + BluetoothProfile.STATE_DISCONNECTED -> { + Timber.tag(tag).i("Disconnected. Closing connection ${gatt.device.address}") + gatt.close() + trySend(Unit) + this@callbackFlow.close() + } + } + } + }) + + awaitClose { + if (isDeviceConnected(device)) { + Timber.tag(tag).e("Device ${gatt.device.address} is still connected") + } + } + }.first() + } + + @SuppressLint("MissingPermission") + suspend fun hardCloseAllConnections(tag: String = TAG_CONNECT) { + connections.values.forEach { close(it, tag) } connections.clear() + val otherConnections = requireBluetoothManager().getConnectedDevices(BluetoothProfile.GATT) + Timber.tag(tag).i("Found ${otherConnections.size} other connections") + otherConnections.forEach { device -> + hardDisconnectDevice(device, tag) + } + System.gc() + val stillConnected = requireBluetoothManager().getConnectedDevices(BluetoothProfile.GATT) + Timber.tag(tag).i("Hard close all connections done. ${stillConnected.size} connections left") } @SuppressLint("MissingPermission") diff --git a/app/src/main/java/f/cking/software/domain/interactor/DeviceServicesFetchingPlanner.kt b/app/src/main/java/f/cking/software/domain/interactor/DeviceServicesFetchingPlanner.kt index 04b748e..3c42eb6 100644 --- a/app/src/main/java/f/cking/software/domain/interactor/DeviceServicesFetchingPlanner.kt +++ b/app/src/main/java/f/cking/software/domain/interactor/DeviceServicesFetchingPlanner.kt @@ -30,11 +30,12 @@ class DeviceServicesFetchingPlanner( private var parallelProcessingBatches = PARALLEL_BATCH_COUNT private var maxPossibleConnections = PARALLEL_BATCH_COUNT - private var cooldown: Long? = null + private var cooldownStartedAt: Long? = null + private var lastJournalReportTime: Long = 0 suspend fun scheduleFetchServiceInfo(devices: List): List = coroutineScope { - val cooldown = this@DeviceServicesFetchingPlanner.cooldown + val cooldown = this@DeviceServicesFetchingPlanner.cooldownStartedAt if (cooldown != null && System.currentTimeMillis() - cooldown < MIN_COOLDOWN_DURATION_MINS.minutes.inWholeMilliseconds) { Timber.tag(TAG).i("Device services fetching is on cooldown due to a high errors rate, current batch will be skipped") return@coroutineScope devices @@ -137,14 +138,31 @@ class DeviceServicesFetchingPlanner( total: Int, ) { Timber.tag(TAG).i("Deep analysis finished. Candidates: $updateNeeded (updated: $updated, timeouts: $timeouts, errors: $errors), total $total devices") - if (updateNeeded > 5 && errors / updateNeeded > 0.7) { - val report = JournalEntry.Report.Error( - title = "Too many errors during deep analysis. Restart bluetooth or disable deep analysis in settings", - stackTrace = "Errors: $errors, timeouts: $timeouts, updated: $updated, in total: $updateNeeded" - ) - saveReportInteractor.execute(report) - cooldown = System.currentTimeMillis() + val errorsRate: Float = errors / (errors + timeouts + updated).toFloat() + if (updateNeeded > 5 && errorsRate > 0.75f) { + Timber.tag(TAG).e("Too many errors during deep analysis. Will try to reset ble stack and remove all connections") + + reportJournalEntity(updateNeeded, updated, timeouts, errors) + bleScannerHelper.hardCloseAllConnections(TAG) + cooldownStartedAt = System.currentTimeMillis() + } + } + + private suspend fun reportJournalEntity( + updateNeeded: Int, + updated: Int, + timeouts: Int, + errors: Int, + ) { + if (System.currentTimeMillis() - lastJournalReportTime < JOURNAL_REPORT_COOLDOWN_MIN.minutes.inWholeMilliseconds) { + return } + val report = JournalEntry.Report.Error( + title = "Too many errors during deep analysis. Restart bluetooth or disable deep analysis in settings", + stackTrace = "Errors: $errors, timeouts: $timeouts, updated: $updated, in total: $updateNeeded" + ) + saveReportInteractor.execute(report) + lastJournalReportTime = System.currentTimeMillis() } private suspend fun softTimeout(timeout: Duration, onTimeout: suspend () -> T, block: suspend () -> T): T = coroutineScope { @@ -182,7 +200,6 @@ class DeviceServicesFetchingPlanner( private fun tooMachConnections() { maxPossibleConnections = parallelProcessingBatches - 1 parallelProcessingBatches = max(1, (parallelProcessingBatches * 0.5).toInt()) - bleScannerHelper.closeAllConnections() } private fun increaseConnections() { @@ -190,11 +207,12 @@ class DeviceServicesFetchingPlanner( } companion object { - private const val PARALLEL_BATCH_COUNT = 10 + private const val PARALLEL_BATCH_COUNT = 7 private const val CHECK_INTERVAL_PER_DEVICE_MIN = 10 + private const val JOURNAL_REPORT_COOLDOWN_MIN = 30 private const val DEVICE_FETCH_TIMEOUT_SEC = 5 private const val TOTAL_FETCH_TIMEOUT_SEC = 30 - private const val MIN_COOLDOWN_DURATION_MINS = 5 + private const val MIN_COOLDOWN_DURATION_MINS = 1 private const val TAG = "DeviceServicesFetchingPlanner" } } \ No newline at end of file diff --git a/app/src/main/java/f/cking/software/domain/interactor/FetchDeviceServiceInfo.kt b/app/src/main/java/f/cking/software/domain/interactor/FetchDeviceServiceInfo.kt index f9ad72c..1af05a2 100644 --- a/app/src/main/java/f/cking/software/domain/interactor/FetchDeviceServiceInfo.kt +++ b/app/src/main/java/f/cking/software/domain/interactor/FetchDeviceServiceInfo.kt @@ -56,6 +56,7 @@ class FetchDeviceServiceInfo( suspend fun submitMetadata() { Timber.tag(TAG).i("Closing connection ${device.address}") gatt?.let(bleScannerHelper::close) + gatt = null // to be sure job?.cancel() emit(metadata) } From ea2ce3a0c7d1d86eec1caf35955169999f803ac3 Mon Sep 17 00:00:00 2001 From: semper-viventem Date: Tue, 1 Apr 2025 17:43:51 -0700 Subject: [PATCH 2/3] Update deep analysis --- .../DeviceServicesFetchingPlanner.kt | 13 ++++---- .../interactor/FetchDeviceServiceInfo.kt | 31 ++++++++++++------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/f/cking/software/domain/interactor/DeviceServicesFetchingPlanner.kt b/app/src/main/java/f/cking/software/domain/interactor/DeviceServicesFetchingPlanner.kt index 3c42eb6..0124d8b 100644 --- a/app/src/main/java/f/cking/software/domain/interactor/DeviceServicesFetchingPlanner.kt +++ b/app/src/main/java/f/cking/software/domain/interactor/DeviceServicesFetchingPlanner.kt @@ -197,20 +197,21 @@ class DeviceServicesFetchingPlanner( || !recentlyChecked } - private fun tooMachConnections() { - maxPossibleConnections = parallelProcessingBatches - 1 - parallelProcessingBatches = max(1, (parallelProcessingBatches * 0.5).toInt()) + private fun decreaseMaxConnections() { + parallelProcessingBatches = max(MIN_PARALLEL_CONNECTIONS, parallelProcessingBatches - 1) } private fun increaseConnections() { - parallelProcessingBatches = min(max(1, (parallelProcessingBatches * 1.2).toInt()), maxPossibleConnections) + parallelProcessingBatches = min(parallelProcessingBatches + 1, MAX_PARALLEL_CONNECTIONS) } companion object { - private const val PARALLEL_BATCH_COUNT = 7 + private const val PARALLEL_BATCH_COUNT = 7 // usually 7 parallel connections are stable + private const val MIN_PARALLEL_CONNECTIONS = 2 + private const val MAX_PARALLEL_CONNECTIONS = 15 private const val CHECK_INTERVAL_PER_DEVICE_MIN = 10 private const val JOURNAL_REPORT_COOLDOWN_MIN = 30 - private const val DEVICE_FETCH_TIMEOUT_SEC = 5 + private const val DEVICE_FETCH_TIMEOUT_SEC = 8 private const val TOTAL_FETCH_TIMEOUT_SEC = 30 private const val MIN_COOLDOWN_DURATION_MINS = 1 private const val TAG = "DeviceServicesFetchingPlanner" diff --git a/app/src/main/java/f/cking/software/domain/interactor/FetchDeviceServiceInfo.kt b/app/src/main/java/f/cking/software/domain/interactor/FetchDeviceServiceInfo.kt index 1af05a2..534c433 100644 --- a/app/src/main/java/f/cking/software/domain/interactor/FetchDeviceServiceInfo.kt +++ b/app/src/main/java/f/cking/software/domain/interactor/FetchDeviceServiceInfo.kt @@ -9,6 +9,7 @@ import f.cking.software.domain.model.DeviceData import f.cking.software.domain.model.DeviceMetadata import f.cking.software.domain.model.DeviceMetadata.CharacteristicType import f.cking.software.domain.model.DeviceMetadata.ServiceTypes +import f.cking.software.domain.model.isNullOrEmpty import f.cking.software.fromBase64 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -72,6 +73,12 @@ class FetchDeviceServiceInfo( } } + fun throwIfMetadataNotUpdated(e: Exception) { + if (metadata.isNullOrEmpty() || metadata == device.metadata) { + throw e + } + } + bleScannerHelper.connectToDevice(device.address) .collect { event -> when (event) { @@ -161,38 +168,38 @@ class FetchDeviceServiceInfo( // Error handling is BleScannerHelper.DeviceConnectResult.DisconnectedWithError.UnspecifiedConnectionError -> { Timber.tag(TAG).e("Unspecified connection error from ${device.address}.") - disconnect(event.gatt) - throw BluetoothConnectionException.UnspecifiedConnectionError(event.errorCode) + throwIfMetadataNotUpdated(BluetoothConnectionException.UnspecifiedConnectionError(event.errorCode)) + submitMetadata() } is BleScannerHelper.DeviceConnectResult.DisconnectedWithError.ConnectionTimeout -> { Timber.tag(TAG).e("Connection timeout error from ${device.address}") - disconnect(event.gatt) - throw BluetoothConnectionException.ConnectionTimeoutException(event.errorCode) + throwIfMetadataNotUpdated(BluetoothConnectionException.ConnectionTimeoutException(event.errorCode)) + submitMetadata() } is BleScannerHelper.DeviceConnectResult.DisconnectedWithError.ConnectionFailedToEstablish -> { Timber.tag(TAG).e("Connection failed to establish error from ${device.address}") - disconnect(event.gatt) - throw BluetoothConnectionException.ConnectionFailedToEstablish(event.errorCode) + throwIfMetadataNotUpdated(BluetoothConnectionException.ConnectionFailedToEstablish(event.errorCode)) + submitMetadata() } is BleScannerHelper.DeviceConnectResult.DisconnectedWithError.ConnectionFailedBeforeInitializing -> { Timber.tag(TAG).e("Connection initializing failed error from ${device.address}") - disconnect(event.gatt) - throw BluetoothConnectionException.ConnectionInitializingFailed(event.errorCode) + throwIfMetadataNotUpdated(BluetoothConnectionException.ConnectionInitializingFailed(event.errorCode)) + submitMetadata() } is BleScannerHelper.DeviceConnectResult.DisconnectedWithError.ConnectionTerminated -> { Timber.tag(TAG).e("Connection terminated error from ${device.address}. Probably max GATT connections reached") - disconnect(event.gatt) - throw BluetoothConnectionException.ConnectionTerminated(event.errorCode) + throwIfMetadataNotUpdated(BluetoothConnectionException.ConnectionTerminated(event.errorCode)) + submitMetadata() } is BleScannerHelper.DeviceConnectResult.DisconnectedWithError.ConnectionFailedTooManyClients -> { Timber.tag(TAG).e("Connection failed due to too many clients error from ${device.address}") - disconnect(event.gatt) - throw BluetoothConnectionException.TooManyClients(event.errorCode) + throwIfMetadataNotUpdated(BluetoothConnectionException.TooManyClients(event.errorCode)) + submitMetadata() } else -> { From 8b7dcc49b9896d2882ce21a34f170e51eaca38a1 Mon Sep 17 00:00:00 2001 From: semper-viventem Date: Tue, 1 Apr 2025 17:46:22 -0700 Subject: [PATCH 3/3] Update deep analysis --- .../software/domain/interactor/DeviceServicesFetchingPlanner.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/f/cking/software/domain/interactor/DeviceServicesFetchingPlanner.kt b/app/src/main/java/f/cking/software/domain/interactor/DeviceServicesFetchingPlanner.kt index 0124d8b..3b50a48 100644 --- a/app/src/main/java/f/cking/software/domain/interactor/DeviceServicesFetchingPlanner.kt +++ b/app/src/main/java/f/cking/software/domain/interactor/DeviceServicesFetchingPlanner.kt @@ -29,7 +29,6 @@ class DeviceServicesFetchingPlanner( ) { private var parallelProcessingBatches = PARALLEL_BATCH_COUNT - private var maxPossibleConnections = PARALLEL_BATCH_COUNT private var cooldownStartedAt: Long? = null private var lastJournalReportTime: Long = 0