Skip to content

Commit 85f39ea

Browse files
Improve deep analysis
1 parent 7e66c7f commit 85f39ea

File tree

5 files changed

+170
-79
lines changed

5 files changed

+170
-79
lines changed

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

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,11 @@ class BleScannerHelper(
9393
return callbackFlow {
9494
val services = mutableSetOf<BluetoothGattService>()
9595
val device = requireAdapter().getRemoteDevice(address)
96+
var gatt: BluetoothGatt? = null
9697

9798
val callback = object : BluetoothGattCallback() {
9899
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
99100
super.onServicesDiscovered(gatt, status)
100-
connections.put(gatt.device.address, gatt)
101-
102101
if (status == BluetoothGatt.GATT_SUCCESS) {
103102
Timber.tag(TAG_CONNECT).d("Services discovered. ${gatt.services.size} services for device $address")
104103
services.addAll(gatt.services.orEmpty())
@@ -148,53 +147,62 @@ class BleScannerHelper(
148147
BluetoothProfile.STATE_DISCONNECTING -> {
149148
Timber.tag(TAG_CONNECT).d("Disconnecting from device $address")
150149
trySend(DeviceConnectResult.Disconnecting)
150+
gatt.close()
151151
}
152152
BluetoothProfile.STATE_DISCONNECTED -> {
153153
Timber.tag(TAG_CONNECT).d("Disconnected from device $address")
154-
handleDisconnect(status)
154+
handleDisconnect(status, gatt)
155155
}
156156
else -> {
157157
Timber.tag(TAG_CONNECT).e("Error while connecting to device $address. Error code: $status")
158-
trySend(DeviceConnectResult.DisconnectedWithError.UnspecifiedConnectionError(status))
158+
trySend(DeviceConnectResult.DisconnectedWithError.UnspecifiedConnectionError(gatt, status))
159159
}
160160
}
161161
}
162162

163-
private fun handleDisconnect(status: Int) {
163+
private fun handleDisconnect(status: Int, gatt: BluetoothGatt) {
164164
when (status) {
165165
BluetoothGatt.GATT_SUCCESS -> {
166166
trySend(DeviceConnectResult.Disconnected)
167167
}
168168
CONNECTION_FAILED_TO_ESTABLISH -> {
169169
Timber.tag(TAG_CONNECT).e("Error while connecting to device $address. Error code: $status")
170-
trySend(DeviceConnectResult.DisconnectedWithError.ConnectionFailedToEstablish(status))
170+
trySend(DeviceConnectResult.DisconnectedWithError.ConnectionFailedToEstablish(gatt, status))
171171
}
172172
CONNECTION_FAILED_BEFORE_INITIALIZING -> {
173173
Timber.tag(TAG_CONNECT).e("Error while connecting to device $address. Error code: $status")
174-
trySend(DeviceConnectResult.DisconnectedWithError.ConnectionFailedBeforeInitializing(status))
174+
trySend(DeviceConnectResult.DisconnectedWithError.ConnectionFailedBeforeInitializing(gatt, status))
175175
}
176176
CONNECTION_TERMINATED -> {
177177
Timber.tag(TAG_CONNECT).e("Error while connecting to device $address. Error code: $status")
178-
trySend(DeviceConnectResult.DisconnectedWithError.ConnectionTerminated(status))
178+
trySend(DeviceConnectResult.DisconnectedWithError.ConnectionTerminated(gatt, status))
179179
}
180180
BluetoothGatt.GATT_CONNECTION_TIMEOUT -> {
181181
Timber.tag(TAG_CONNECT).e("Error while connecting to device $address. Error code: $status")
182-
trySend(DeviceConnectResult.DisconnectedWithError.ConnectionTimeout(status))
182+
trySend(DeviceConnectResult.DisconnectedWithError.ConnectionTimeout(gatt, status))
183+
}
184+
BluetoothGatt.GATT_FAILURE -> {
185+
Timber.tag(TAG_CONNECT).e("Error while connecting to device $address. Error code: $status")
186+
trySend(DeviceConnectResult.DisconnectedWithError.ConnectionFailedTooManyClients(gatt, status))
183187
}
184188
else -> {
185189
Timber.tag(TAG_CONNECT).e("Error while connecting to device $address. Error code: $status")
186-
trySend(DeviceConnectResult.DisconnectedWithError.UnspecifiedConnectionError(status))
190+
trySend(DeviceConnectResult.DisconnectedWithError.UnspecifiedConnectionError(gatt, status))
187191
}
188192
}
189193
}
190194
}
191195

192196
Timber.tag(TAG_CONNECT).d("Connecting to device $address")
193-
connections[address] = device.connectGatt(appContext, false, callback, BluetoothDevice.TRANSPORT_LE)
197+
gatt = device.connectGatt(appContext, false, callback, BluetoothDevice.TRANSPORT_LE)
194198

195199
awaitClose {
196200
Timber.tag(TAG_CONNECT).d("Closing connection to device $address")
197-
closeDeviceConnection(address)
201+
if (requireBluetoothManager().getConnectionState(device, BluetoothProfile.GATT) != BluetoothProfile.STATE_DISCONNECTED) {
202+
gatt.disconnect()
203+
} else {
204+
gatt.close()
205+
}
198206
}
199207
}
200208
}
@@ -254,12 +262,14 @@ class BleScannerHelper(
254262
data object Disconnected : DeviceConnectResult
255263
sealed interface DisconnectedWithError : DeviceConnectResult {
256264
val errorCode: Int
257-
258-
class UnspecifiedConnectionError(override val errorCode: Int) : DisconnectedWithError
259-
class ConnectionTimeout(override val errorCode: Int) : DisconnectedWithError
260-
class ConnectionTerminated(override val errorCode: Int) : DisconnectedWithError
261-
class ConnectionFailedToEstablish(override val errorCode: Int) : DisconnectedWithError
262-
class ConnectionFailedBeforeInitializing(override val errorCode: Int) : DisconnectedWithError
265+
val gatt: BluetoothGatt
266+
267+
class UnspecifiedConnectionError(override val gatt: BluetoothGatt, override val errorCode: Int) : DisconnectedWithError
268+
class ConnectionTimeout(override val gatt: BluetoothGatt, override val errorCode: Int) : DisconnectedWithError
269+
class ConnectionTerminated(override val gatt: BluetoothGatt, override val errorCode: Int) : DisconnectedWithError
270+
class ConnectionFailedToEstablish(override val gatt: BluetoothGatt, override val errorCode: Int) : DisconnectedWithError
271+
class ConnectionFailedBeforeInitializing(override val gatt: BluetoothGatt, override val errorCode: Int) : DisconnectedWithError
272+
class ConnectionFailedTooManyClients(override val gatt: BluetoothGatt, override val errorCode: Int) : DisconnectedWithError
263273
}
264274
}
265275

@@ -336,10 +346,14 @@ class BleScannerHelper(
336346
}
337347

338348
private fun tryToInitBluetoothScanner() {
339-
bluetoothAdapter = appContext.getSystemService(BluetoothManager::class.java).adapter
349+
bluetoothAdapter = requireBluetoothManager().adapter
340350
bluetoothScanner = bluetoothAdapter?.bluetoothLeScanner
341351
}
342352

353+
private fun requireBluetoothManager(): BluetoothManager {
354+
return appContext.getSystemService(BluetoothManager::class.java)
355+
}
356+
343357
private fun requireScanner(): BluetoothLeScanner {
344358
if (bluetoothScanner == null) {
345359
tryToInitBluetoothScanner()

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

Lines changed: 115 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package f.cking.software.domain.interactor
22

33
import f.cking.software.data.helpers.BleScannerHelper
44
import f.cking.software.domain.model.DeviceData
5+
import f.cking.software.domain.model.JournalEntry
56
import f.cking.software.domain.model.SavedDeviceHandle
67
import f.cking.software.domain.model.isNullOrEmpty
78
import f.cking.software.mapParallel
@@ -11,7 +12,8 @@ import kotlinx.coroutines.Job
1112
import kotlinx.coroutines.async
1213
import kotlinx.coroutines.coroutineScope
1314
import kotlinx.coroutines.delay
14-
import kotlinx.coroutines.launch
15+
import kotlinx.coroutines.flow.channelFlow
16+
import kotlinx.coroutines.flow.first
1517
import kotlinx.coroutines.withContext
1618
import timber.log.Timber
1719
import kotlin.math.max
@@ -23,89 +25,145 @@ import kotlin.time.Duration.Companion.seconds
2325
class DeviceServicesFetchingPlanner(
2426
private val fetchDeviceServiceInfo: FetchDeviceServiceInfo,
2527
private val bleScannerHelper: BleScannerHelper,
28+
private val saveReportInteractor: SaveReportInteractor,
2629
) {
2730

28-
private var currentJob: Job? = null
2931
private var parallelProcessingBatches = PARALLEL_BATCH_COUNT
3032
private var maxPossibleConnections = PARALLEL_BATCH_COUNT
33+
private var cooldown: Long? = null
3134

32-
suspend fun scheduleFetchServiceInfo(devices: List<SavedDeviceHandle>) = coroutineScope {
33-
currentJob?.cancel()
34-
currentJob = launch {
35-
withContext(Dispatchers.IO) {
36-
val metadataNeeded = devices
37-
.filter { checkIfMetadataUpdateNeeded(it) }
38-
.map { it.device }
39-
.sortedBy { it.rssi }
40-
.reversed()
41-
42-
Timber.tag(TAG).i("Scheduling fetch service info for ${metadataNeeded.size} devices, out of ${devices.size} total")
43-
try {
44-
fetchAllDevices(metadataNeeded)
45-
// increaseConnections()
46-
} catch (e: FetchDeviceServiceInfo.BluetoothConnectionException.UnspecifiedConnectionError) {
47-
Timber.tag(TAG).e(e, "Max connections reached")
48-
// currentJob?.cancel()
49-
// tooMachConnections()
35+
suspend fun scheduleFetchServiceInfo(devices: List<SavedDeviceHandle>): List<SavedDeviceHandle> = coroutineScope {
36+
37+
val cooldown = cooldown
38+
if (cooldown != null && System.currentTimeMillis() - cooldown < MIN_COOLDOWN_DURATION_SEC.seconds.inWholeMilliseconds) {
39+
Timber.tag(TAG).i("Device services fetching is on cooldown due to a high errors rate, current batch will be skipped")
40+
return@coroutineScope devices
41+
}
42+
43+
val result = devices
44+
.map { it }
45+
.associateBy { it.device.address }
46+
.toMutableMap()
47+
48+
var updatedCount = 0
49+
var errors = 0
50+
var timeouts = 0
51+
52+
withContext(Dispatchers.IO) {
53+
val metadataNeeded = devices
54+
.filter { checkIfMetadataUpdateNeeded(it) }
55+
.map { it.device }
56+
.sortedBy { it.rssi }
57+
.reversed()
58+
59+
Timber.tag(TAG).i("Scheduling fetch service info for ${metadataNeeded.size} devices, out of ${devices.size} total")
60+
val updated = fetchAllDevices(metadataNeeded)
61+
updated.forEach { fetchFeedback ->
62+
when (fetchFeedback.feedback) {
63+
FetchFeedback.SUCCESS -> {
64+
fetchFeedback.device?.let { device ->
65+
result[device.address]?.let { previousHandle ->
66+
result[device.address] = previousHandle.copy(device = device)
67+
}
68+
updatedCount++
69+
}
70+
}
71+
FetchFeedback.TIMEOUT -> {
72+
timeouts++
73+
}
74+
FetchFeedback.ERROR -> {
75+
errors++
76+
}
5077
}
5178
}
52-
}
53-
}
5479

55-
private fun tooMachConnections() {
56-
maxPossibleConnections = parallelProcessingBatches - 1
57-
parallelProcessingBatches = max(1, (parallelProcessingBatches * 0.5).toInt())
58-
bleScannerHelper.closeAllConnections()
80+
analyzeFeedback(metadataNeeded.size, updatedCount, timeouts, errors, devices.size)
81+
result.values.toList()
82+
}
5983
}
6084

61-
private fun increaseConnections() {
62-
parallelProcessingBatches = min(max(1, (parallelProcessingBatches * 1.2).toInt()), maxPossibleConnections)
63-
}
85+
private suspend fun fetchAllDevices(metadataNeeded: List<DeviceData>): List<DeviceFetchFeedback> = coroutineScope {
86+
val result = mutableListOf<DeviceFetchFeedback>()
6487

65-
private suspend fun fetchAllDevices(metadataNeeded: List<DeviceData>) = coroutineScope {
6688
softTimeout(TOTAL_FETCH_TIMEOUT_SEC.seconds, onTimeout = {
6789
Timber.tag(TAG).e("Timeout fetching total devices")
6890
}) {
6991
metadataNeeded.splitToBatchesEqual(parallelProcessingBatches)
7092
.filter { it.isNotEmpty() }
7193
.mapParallel { batch ->
7294
Timber.tag(TAG).i("Processing batch of ${batch.size} devices ($parallelProcessingBatches parallel)")
73-
batch.forEach { device ->
74-
fetchDevice(device)
95+
batch.map { device ->
96+
result += fetchDevice(device)
7597
}
7698
}
77-
Timber.tag(TAG).i("All devices processed")
7899
}
100+
101+
result
79102
}
80103

81-
private suspend fun fetchDevice(device: DeviceData) {
82-
softTimeout(DEVICE_FETCH_TIMEOUT_SEC.seconds, onTimeout = {
104+
private suspend fun fetchDevice(device: DeviceData): DeviceFetchFeedback {
105+
return softTimeout(DEVICE_FETCH_TIMEOUT_SEC.seconds, onTimeout = {
83106
Timber.tag(TAG).e("Timeout fetching device info for ${device.address}")
107+
DeviceFetchFeedback(null, FetchFeedback.TIMEOUT)
84108
}) {
85109
Timber.tag(TAG).i("Fetching device info for ${device.address}, distance: ${device.distance()}")
86110
try {
87111
val result = fetchDeviceServiceInfo.execute(device)
88112
Timber.tag(TAG).i("Fetching complete for ${device.address}. Result: $result")
113+
DeviceFetchFeedback(result?.let { device.copy(metadata = it) }, FetchFeedback.SUCCESS)
89114
} catch (e: FetchDeviceServiceInfo.BluetoothConnectionException) {
90115
Timber.tag(TAG).e(e, "Error when connecting to device ${device.address}")
116+
DeviceFetchFeedback(null, FetchFeedback.ERROR)
91117
}
92118
}
93119
}
94120

95-
private suspend fun softTimeout(timeout: Duration, onTimeout: suspend () -> Unit, block: suspend () -> Unit) = coroutineScope {
96-
var timeoutJob: Job? = null
97-
val primaryJob = async {
98-
block.invoke()
99-
timeoutJob?.cancel()
100-
}
121+
private data class DeviceFetchFeedback(
122+
val device: DeviceData?,
123+
val feedback: FetchFeedback,
124+
)
101125

102-
timeoutJob = async {
103-
delay(timeout)
104-
primaryJob.cancel()
105-
onTimeout.invoke()
126+
private enum class FetchFeedback {
127+
TIMEOUT,
128+
ERROR,
129+
SUCCESS,
130+
}
131+
132+
private suspend fun analyzeFeedback(
133+
updateNeeded: Int,
134+
updated: Int,
135+
timeouts: Int,
136+
errors: Int,
137+
total: Int,
138+
) {
139+
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()
106147
}
107148
}
108149

150+
private suspend fun <T> softTimeout(timeout: Duration, onTimeout: suspend () -> T, block: suspend () -> T): T = coroutineScope {
151+
channelFlow<T> {
152+
var timeoutJob: Job? = null
153+
val primaryJob = async {
154+
val result = block.invoke()
155+
timeoutJob?.cancel()
156+
send(result)
157+
}
158+
159+
timeoutJob = async {
160+
delay(timeout)
161+
primaryJob.cancel()
162+
send(onTimeout.invoke())
163+
}
164+
}.first()
165+
}
166+
109167
private fun checkIfMetadataUpdateNeeded(device: SavedDeviceHandle): Boolean {
110168
if (!device.device.isConnectable) {
111169
return false
@@ -121,11 +179,22 @@ class DeviceServicesFetchingPlanner(
121179
|| !recentlyChecked
122180
}
123181

182+
private fun tooMachConnections() {
183+
maxPossibleConnections = parallelProcessingBatches - 1
184+
parallelProcessingBatches = max(1, (parallelProcessingBatches * 0.5).toInt())
185+
bleScannerHelper.closeAllConnections()
186+
}
187+
188+
private fun increaseConnections() {
189+
parallelProcessingBatches = min(max(1, (parallelProcessingBatches * 1.2).toInt()), maxPossibleConnections)
190+
}
191+
124192
companion object {
125-
private const val PARALLEL_BATCH_COUNT = 6
193+
private const val PARALLEL_BATCH_COUNT = 10
126194
private const val CHECK_INTERVAL_PER_DEVICE_MIN = 10
127195
private const val DEVICE_FETCH_TIMEOUT_SEC = 5
128196
private const val TOTAL_FETCH_TIMEOUT_SEC = 30
197+
private const val MIN_COOLDOWN_DURATION_SEC = 60
129198
private const val TAG = "DeviceServicesFetchingPlanner"
130199
}
131200
}

0 commit comments

Comments
 (0)