Skip to content

Commit f18c5e9

Browse files
Merge pull request #188 from Semper-Viventem/improve-deep-analysis
Improve deep analysis
2 parents 000c49e + 8b7dcc4 commit f18c5e9

File tree

4 files changed

+120
-40
lines changed

4 files changed

+120
-40
lines changed

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ android {
2626
minSdk = 29
2727
targetSdk = 35
2828

29-
versionCode = 1708536372
30-
versionName = "0.29.2-beta"
29+
versionCode = 1708536373
30+
versionName = "0.29.3-beta"
3131

3232
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
3333

app/src/main/java/f/cking/software/data/helpers/BleScannerHelper.kt

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import kotlinx.coroutines.channels.awaitClose
2626
import kotlinx.coroutines.flow.Flow
2727
import kotlinx.coroutines.flow.MutableStateFlow
2828
import kotlinx.coroutines.flow.callbackFlow
29+
import kotlinx.coroutines.flow.first
2930
import kotlinx.coroutines.withContext
3031
import timber.log.Timber
3132
import java.util.UUID
@@ -147,11 +148,11 @@ class BleScannerHelper(
147148
BluetoothProfile.STATE_DISCONNECTING -> {
148149
Timber.tag(TAG_CONNECT).d("Disconnecting from device $address")
149150
trySend(DeviceConnectResult.Disconnecting)
150-
gatt.close()
151151
}
152152
BluetoothProfile.STATE_DISCONNECTED -> {
153153
Timber.tag(TAG_CONNECT).d("Disconnected from device $address")
154154
handleDisconnect(status, gatt)
155+
close(gatt)
155156
}
156157
else -> {
157158
Timber.tag(TAG_CONNECT).e("Error while connecting to device $address. Error code: $status")
@@ -198,10 +199,10 @@ class BleScannerHelper(
198199

199200
awaitClose {
200201
Timber.tag(TAG_CONNECT).d("Closing connection to device $address")
201-
if (requireBluetoothManager().getConnectionState(device, BluetoothProfile.GATT) != BluetoothProfile.STATE_DISCONNECTED) {
202+
if (isDeviceConnected(device)) {
202203
gatt.disconnect()
203204
} else {
204-
gatt.close()
205+
close(gatt)
205206
}
206207
}
207208
}
@@ -220,19 +221,72 @@ class BleScannerHelper(
220221
}
221222

222223
@SuppressLint("MissingPermission")
223-
fun close(gatt: BluetoothGatt) {
224-
Timber.tag(TAG_CONNECT).d("Closing connection to device ${gatt.device.address}")
224+
fun close(gatt: BluetoothGatt, tag: String = TAG_CONNECT) {
225+
Timber.tag(tag).i("Closing connection to device ${gatt.device.address}")
226+
if (isDeviceConnected(gatt.device)) {
227+
Timber.tag(tag).e("Trying to close connection for device ${gatt.device.address} while it is still connected.")
228+
}
225229
gatt.close()
230+
connections.remove(gatt.device.address)
226231
}
227232

228233
fun closeDeviceConnection(address: String) {
229234
connections[address]?.let(::close)
230-
connections.remove(address)
231235
}
232236

233-
fun closeAllConnections() {
234-
connections.values.forEach(::close)
237+
@SuppressLint("MissingPermission")
238+
fun isDeviceConnected(device: BluetoothDevice): Boolean {
239+
return requireBluetoothManager().getConnectionState(device, BluetoothProfile.GATT) == BluetoothProfile.STATE_CONNECTED
240+
}
241+
242+
@SuppressLint("MissingPermission")
243+
fun isDeviceDisconnected(device: BluetoothDevice): Boolean {
244+
return requireBluetoothManager().getConnectionState(device, BluetoothProfile.GATT) == BluetoothProfile.STATE_DISCONNECTED
245+
}
246+
247+
@SuppressLint("MissingPermission")
248+
suspend fun hardDisconnectDevice(device: BluetoothDevice, tag: String = TAG_CONNECT) {
249+
return callbackFlow<Unit> {
250+
Timber.tag(tag).i("Trying to close connection to device ${device.address}")
251+
val gatt = device.connectGatt(appContext, false, object : BluetoothGattCallback() {
252+
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
253+
super.onConnectionStateChange(gatt, status, newState)
254+
Timber.tag(tag).i("Connection state change for device ${gatt.device.address}. Status: $status, newState: $newState")
255+
when (newState) {
256+
BluetoothProfile.STATE_CONNECTED -> {
257+
Timber.tag(tag).i("Try disconnect from ${gatt.device.address}")
258+
gatt.disconnect()
259+
}
260+
BluetoothProfile.STATE_DISCONNECTED -> {
261+
Timber.tag(tag).i("Disconnected. Closing connection ${gatt.device.address}")
262+
gatt.close()
263+
trySend(Unit)
264+
this@callbackFlow.close()
265+
}
266+
}
267+
}
268+
})
269+
270+
awaitClose {
271+
if (isDeviceConnected(device)) {
272+
Timber.tag(tag).e("Device ${gatt.device.address} is still connected")
273+
}
274+
}
275+
}.first()
276+
}
277+
278+
@SuppressLint("MissingPermission")
279+
suspend fun hardCloseAllConnections(tag: String = TAG_CONNECT) {
280+
connections.values.forEach { close(it, tag) }
235281
connections.clear()
282+
val otherConnections = requireBluetoothManager().getConnectedDevices(BluetoothProfile.GATT)
283+
Timber.tag(tag).i("Found ${otherConnections.size} other connections")
284+
otherConnections.forEach { device ->
285+
hardDisconnectDevice(device, tag)
286+
}
287+
System.gc()
288+
val stillConnected = requireBluetoothManager().getConnectedDevices(BluetoothProfile.GATT)
289+
Timber.tag(tag).i("Hard close all connections done. ${stillConnected.size} connections left")
236290
}
237291

238292
@SuppressLint("MissingPermission")

app/src/main/java/f/cking/software/domain/interactor/DeviceServicesFetchingPlanner.kt

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ class DeviceServicesFetchingPlanner(
2929
) {
3030

3131
private var parallelProcessingBatches = PARALLEL_BATCH_COUNT
32-
private var maxPossibleConnections = PARALLEL_BATCH_COUNT
33-
private var cooldown: Long? = null
32+
private var cooldownStartedAt: Long? = null
33+
private var lastJournalReportTime: Long = 0
3434

3535
suspend fun scheduleFetchServiceInfo(devices: List<SavedDeviceHandle>): List<SavedDeviceHandle> = coroutineScope {
3636

37-
val cooldown = this@DeviceServicesFetchingPlanner.cooldown
37+
val cooldown = this@DeviceServicesFetchingPlanner.cooldownStartedAt
3838
if (cooldown != null && System.currentTimeMillis() - cooldown < MIN_COOLDOWN_DURATION_MINS.minutes.inWholeMilliseconds) {
3939
Timber.tag(TAG).i("Device services fetching is on cooldown due to a high errors rate, current batch will be skipped")
4040
return@coroutineScope devices
@@ -137,14 +137,31 @@ class DeviceServicesFetchingPlanner(
137137
total: Int,
138138
) {
139139
Timber.tag(TAG).i("Deep analysis finished. Candidates: $updateNeeded (updated: $updated, timeouts: $timeouts, errors: $errors), total $total devices")
140-
if (updateNeeded > 5 && errors / updateNeeded > 0.7) {
141-
val report = JournalEntry.Report.Error(
142-
title = "Too many errors during deep analysis. Restart bluetooth or disable deep analysis in settings",
143-
stackTrace = "Errors: $errors, timeouts: $timeouts, updated: $updated, in total: $updateNeeded"
144-
)
145-
saveReportInteractor.execute(report)
146-
cooldown = System.currentTimeMillis()
140+
val errorsRate: Float = errors / (errors + timeouts + updated).toFloat()
141+
if (updateNeeded > 5 && errorsRate > 0.75f) {
142+
Timber.tag(TAG).e("Too many errors during deep analysis. Will try to reset ble stack and remove all connections")
143+
144+
reportJournalEntity(updateNeeded, updated, timeouts, errors)
145+
bleScannerHelper.hardCloseAllConnections(TAG)
146+
cooldownStartedAt = System.currentTimeMillis()
147+
}
148+
}
149+
150+
private suspend fun reportJournalEntity(
151+
updateNeeded: Int,
152+
updated: Int,
153+
timeouts: Int,
154+
errors: Int,
155+
) {
156+
if (System.currentTimeMillis() - lastJournalReportTime < JOURNAL_REPORT_COOLDOWN_MIN.minutes.inWholeMilliseconds) {
157+
return
147158
}
159+
val report = JournalEntry.Report.Error(
160+
title = "Too many errors during deep analysis. Restart bluetooth or disable deep analysis in settings",
161+
stackTrace = "Errors: $errors, timeouts: $timeouts, updated: $updated, in total: $updateNeeded"
162+
)
163+
saveReportInteractor.execute(report)
164+
lastJournalReportTime = System.currentTimeMillis()
148165
}
149166

150167
private suspend fun <T> softTimeout(timeout: Duration, onTimeout: suspend () -> T, block: suspend () -> T): T = coroutineScope {
@@ -179,22 +196,23 @@ class DeviceServicesFetchingPlanner(
179196
|| !recentlyChecked
180197
}
181198

182-
private fun tooMachConnections() {
183-
maxPossibleConnections = parallelProcessingBatches - 1
184-
parallelProcessingBatches = max(1, (parallelProcessingBatches * 0.5).toInt())
185-
bleScannerHelper.closeAllConnections()
199+
private fun decreaseMaxConnections() {
200+
parallelProcessingBatches = max(MIN_PARALLEL_CONNECTIONS, parallelProcessingBatches - 1)
186201
}
187202

188203
private fun increaseConnections() {
189-
parallelProcessingBatches = min(max(1, (parallelProcessingBatches * 1.2).toInt()), maxPossibleConnections)
204+
parallelProcessingBatches = min(parallelProcessingBatches + 1, MAX_PARALLEL_CONNECTIONS)
190205
}
191206

192207
companion object {
193-
private const val PARALLEL_BATCH_COUNT = 10
208+
private const val PARALLEL_BATCH_COUNT = 7 // usually 7 parallel connections are stable
209+
private const val MIN_PARALLEL_CONNECTIONS = 2
210+
private const val MAX_PARALLEL_CONNECTIONS = 15
194211
private const val CHECK_INTERVAL_PER_DEVICE_MIN = 10
195-
private const val DEVICE_FETCH_TIMEOUT_SEC = 5
212+
private const val JOURNAL_REPORT_COOLDOWN_MIN = 30
213+
private const val DEVICE_FETCH_TIMEOUT_SEC = 8
196214
private const val TOTAL_FETCH_TIMEOUT_SEC = 30
197-
private const val MIN_COOLDOWN_DURATION_MINS = 5
215+
private const val MIN_COOLDOWN_DURATION_MINS = 1
198216
private const val TAG = "DeviceServicesFetchingPlanner"
199217
}
200218
}

app/src/main/java/f/cking/software/domain/interactor/FetchDeviceServiceInfo.kt

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import f.cking.software.domain.model.DeviceData
99
import f.cking.software.domain.model.DeviceMetadata
1010
import f.cking.software.domain.model.DeviceMetadata.CharacteristicType
1111
import f.cking.software.domain.model.DeviceMetadata.ServiceTypes
12+
import f.cking.software.domain.model.isNullOrEmpty
1213
import f.cking.software.fromBase64
1314
import kotlinx.coroutines.Dispatchers
1415
import kotlinx.coroutines.FlowPreview
@@ -56,6 +57,7 @@ class FetchDeviceServiceInfo(
5657
suspend fun submitMetadata() {
5758
Timber.tag(TAG).i("Closing connection ${device.address}")
5859
gatt?.let(bleScannerHelper::close)
60+
gatt = null // to be sure
5961
job?.cancel()
6062
emit(metadata)
6163
}
@@ -71,6 +73,12 @@ class FetchDeviceServiceInfo(
7173
}
7274
}
7375

76+
fun throwIfMetadataNotUpdated(e: Exception) {
77+
if (metadata.isNullOrEmpty() || metadata == device.metadata) {
78+
throw e
79+
}
80+
}
81+
7482
bleScannerHelper.connectToDevice(device.address)
7583
.collect { event ->
7684
when (event) {
@@ -160,38 +168,38 @@ class FetchDeviceServiceInfo(
160168
// Error handling
161169
is BleScannerHelper.DeviceConnectResult.DisconnectedWithError.UnspecifiedConnectionError -> {
162170
Timber.tag(TAG).e("Unspecified connection error from ${device.address}.")
163-
disconnect(event.gatt)
164-
throw BluetoothConnectionException.UnspecifiedConnectionError(event.errorCode)
171+
throwIfMetadataNotUpdated(BluetoothConnectionException.UnspecifiedConnectionError(event.errorCode))
172+
submitMetadata()
165173
}
166174

167175
is BleScannerHelper.DeviceConnectResult.DisconnectedWithError.ConnectionTimeout -> {
168176
Timber.tag(TAG).e("Connection timeout error from ${device.address}")
169-
disconnect(event.gatt)
170-
throw BluetoothConnectionException.ConnectionTimeoutException(event.errorCode)
177+
throwIfMetadataNotUpdated(BluetoothConnectionException.ConnectionTimeoutException(event.errorCode))
178+
submitMetadata()
171179
}
172180

173181
is BleScannerHelper.DeviceConnectResult.DisconnectedWithError.ConnectionFailedToEstablish -> {
174182
Timber.tag(TAG).e("Connection failed to establish error from ${device.address}")
175-
disconnect(event.gatt)
176-
throw BluetoothConnectionException.ConnectionFailedToEstablish(event.errorCode)
183+
throwIfMetadataNotUpdated(BluetoothConnectionException.ConnectionFailedToEstablish(event.errorCode))
184+
submitMetadata()
177185
}
178186

179187
is BleScannerHelper.DeviceConnectResult.DisconnectedWithError.ConnectionFailedBeforeInitializing -> {
180188
Timber.tag(TAG).e("Connection initializing failed error from ${device.address}")
181-
disconnect(event.gatt)
182-
throw BluetoothConnectionException.ConnectionInitializingFailed(event.errorCode)
189+
throwIfMetadataNotUpdated(BluetoothConnectionException.ConnectionInitializingFailed(event.errorCode))
190+
submitMetadata()
183191
}
184192

185193
is BleScannerHelper.DeviceConnectResult.DisconnectedWithError.ConnectionTerminated -> {
186194
Timber.tag(TAG).e("Connection terminated error from ${device.address}. Probably max GATT connections reached")
187-
disconnect(event.gatt)
188-
throw BluetoothConnectionException.ConnectionTerminated(event.errorCode)
195+
throwIfMetadataNotUpdated(BluetoothConnectionException.ConnectionTerminated(event.errorCode))
196+
submitMetadata()
189197
}
190198

191199
is BleScannerHelper.DeviceConnectResult.DisconnectedWithError.ConnectionFailedTooManyClients -> {
192200
Timber.tag(TAG).e("Connection failed due to too many clients error from ${device.address}")
193-
disconnect(event.gatt)
194-
throw BluetoothConnectionException.TooManyClients(event.errorCode)
201+
throwIfMetadataNotUpdated(BluetoothConnectionException.TooManyClients(event.errorCode))
202+
submitMetadata()
195203
}
196204

197205
else -> {

0 commit comments

Comments
 (0)