From d917c8ffe6d71c156316390264bbd63cfec63f63 Mon Sep 17 00:00:00 2001 From: Matthias Urhahn Date: Wed, 25 Jan 2023 16:27:29 +0100 Subject: [PATCH] New compatibility option for receiving BLE data (#82) * Improve compat options: Add alternative method for receiving BLE scan results via PendingIntents * Add missing logtags * Reduce log spam * Make the linter happy --- app-common/src/main/AndroidManifest.xml | 9 ++ .../bluetooth/BleScanResultForwarder.kt | 33 +++++ .../common/bluetooth/BleScanResultReceiver.kt | 64 ++++++++++ .../capod/common/bluetooth/BleScanner.kt | 118 ++++++++++++------ .../notifications/PendingIntentCompat.kt | 5 + .../darken/capod/main/core/GeneralSettings.kt | 8 +- .../darken/capod/monitor/core/PodMonitor.kt | 18 ++- .../src/main/res/drawable/ic_strategy_24.xml | 10 ++ .../core/receiver/BluetoothEventReceiver.kt | 8 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/preferences_general.xml | 6 + 11 files changed, 236 insertions(+), 45 deletions(-) create mode 100644 app-common/src/main/java/eu/darken/capod/common/bluetooth/BleScanResultForwarder.kt create mode 100644 app-common/src/main/java/eu/darken/capod/common/bluetooth/BleScanResultReceiver.kt rename {app => app-common}/src/main/java/eu/darken/capod/common/notifications/PendingIntentCompat.kt (70%) create mode 100644 app-common/src/main/res/drawable/ic_strategy_24.xml diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml index da23cf8f..beda55e9 100644 --- a/app-common/src/main/AndroidManifest.xml +++ b/app-common/src/main/AndroidManifest.xml @@ -23,4 +23,13 @@ android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> + + + + + + + \ No newline at end of file diff --git a/app-common/src/main/java/eu/darken/capod/common/bluetooth/BleScanResultForwarder.kt b/app-common/src/main/java/eu/darken/capod/common/bluetooth/BleScanResultForwarder.kt new file mode 100644 index 00000000..aeb9fd97 --- /dev/null +++ b/app-common/src/main/java/eu/darken/capod/common/bluetooth/BleScanResultForwarder.kt @@ -0,0 +1,33 @@ +package eu.darken.capod.common.bluetooth + +import android.bluetooth.le.ScanResult +import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE +import eu.darken.capod.common.debug.logging.Logging.Priority.WARN +import eu.darken.capod.common.debug.logging.log +import eu.darken.capod.common.debug.logging.logTag +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class BleScanResultForwarder @Inject constructor() { + + private val forwarder = MutableSharedFlow>( + replay = 0, + extraBufferCapacity = 128, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val results: Flow> = forwarder + + fun forward(scanResults: Collection) { + log(TAG, VERBOSE) { "forward($scanResults)" } + val success = forwarder.tryEmit(scanResults) + if (!success) log(TAG, WARN) { "Failed to forward (overflow?) $scanResults" } + } + + companion object { + private val TAG = logTag("Bluetooth", "BleScanner", "Forwarder") + } +} \ No newline at end of file diff --git a/app-common/src/main/java/eu/darken/capod/common/bluetooth/BleScanResultReceiver.kt b/app-common/src/main/java/eu/darken/capod/common/bluetooth/BleScanResultReceiver.kt new file mode 100644 index 00000000..897d2174 --- /dev/null +++ b/app-common/src/main/java/eu/darken/capod/common/bluetooth/BleScanResultReceiver.kt @@ -0,0 +1,64 @@ +package eu.darken.capod.common.bluetooth + +import android.bluetooth.le.BluetoothLeScanner +import android.bluetooth.le.ScanResult +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import dagger.hilt.android.AndroidEntryPoint +import eu.darken.capod.common.coroutine.AppScope +import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE +import eu.darken.capod.common.debug.logging.Logging.Priority.WARN +import eu.darken.capod.common.debug.logging.log +import eu.darken.capod.common.debug.logging.logTag +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class BleScanResultReceiver : BroadcastReceiver() { + + @Inject @AppScope lateinit var appScope: CoroutineScope + @Inject lateinit var scanResultForwarder: BleScanResultForwarder + + override fun onReceive(context: Context, intent: Intent) { + log(TAG, VERBOSE) { "onReceive($context, $intent)" } + if (intent.action != ACTION) { + log(TAG, WARN) { "Unknown action: ${intent.action}" } + return + } + if (intent.extras == null) { + log(TAG) { "Extras are null!" } + return + } + + val errorCode = intent.getIntExtra(BluetoothLeScanner.EXTRA_ERROR_CODE, 0) + log(TAG, VERBOSE) { "errorCode=$errorCode" } + if (errorCode != 0) { + log(TAG, WARN) { "ScanCallback error code: $errorCode" } + return + } + + val callbackType = intent.getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, -1) + log(TAG, VERBOSE) { "callbackType=$callbackType" } + + val scanResults = intent.getParcelableArrayListExtra(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT) + log(TAG, VERBOSE) { "scanResults=$scanResults" } + + if (scanResults == null) { + log(TAG) { "Scan results were empty!" } + return + } + + val pending = goAsync() + appScope.launch { + scanResultForwarder.forward(scanResults) + pending.finish() + } + } + + companion object { + private val TAG = logTag("Bluetooth", "BleScanner", "Forwarder", "Receiver") + const val ACTION = "eu.darken.capod.bluetooth.DELIVER_SCAN_RESULTS" + } +} diff --git a/app-common/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt b/app-common/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt index 45048101..e07ba886 100644 --- a/app-common/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt +++ b/app-common/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt @@ -1,20 +1,21 @@ package eu.darken.capod.common.bluetooth import android.annotation.SuppressLint +import android.app.PendingIntent import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanFilter import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanSettings import android.content.Context +import android.content.Intent import dagger.hilt.android.qualifiers.ApplicationContext import eu.darken.capod.common.debug.logging.Logging.Priority.* import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag +import eu.darken.capod.common.notifications.PendingIntentCompat import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.* import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import javax.inject.Inject @@ -25,13 +26,15 @@ class BleScanner @Inject constructor( @ApplicationContext private val context: Context, private val bluetoothManager: BluetoothManager2, private val fakeBleData: FakeBleData, + private val scanResultForwarder: BleScanResultForwarder, ) { @SuppressLint("MissingPermission") fun scan( filters: Set, scannerMode: ScannerMode = ScannerMode.BALANCED, - offloadFiltering: Boolean = true, - offloadBatching: Boolean = true, + disableOffloadFiltering: Boolean = true, + disableOffloadBatching: Boolean = true, + disableDirectScanCallback: Boolean = true, ): Flow> = callbackFlow { log(TAG) { "scan(filters=$filters, scannerMode=$scannerMode)" } @@ -39,17 +42,19 @@ class BleScanner @Inject constructor( val useOffloadedFiltering = adapter.isOffloadedFilteringSupported.also { log(TAG, if (it) DEBUG else WARN) { "isOffloadedFilteringSupported=$it" } - } && offloadFiltering - if (!offloadFiltering) log(TAG, WARN) { "Offloaded filtering is disabled!" } + } && !disableOffloadFiltering + if (disableOffloadFiltering) log(TAG, WARN) { "Offloaded filtering is disabled!" } val useOffloadedBatching = adapter.isOffloadedScanBatchingSupported.also { log(TAG, if (it) DEBUG else WARN) { "isOffloadedScanBatchingSupported=$it" } - } && offloadBatching - if (!offloadBatching) log(TAG, WARN) { "Offloaded scan-batching is disabled!" } + } && !disableOffloadBatching + if (disableOffloadBatching) log(TAG, WARN) { "Offloaded scan-batching is disabled!" } + + if (disableDirectScanCallback) log(TAG, WARN) { "Direct scan callback is disabled!" } val scanner = bluetoothManager.scanner ?: throw IllegalStateException("BLE scanner unavailable") - val resultFilter: (Collection) -> Collection = { results -> + val filterResults: (Collection) -> Collection = { results -> results .filter { result -> val passed = when { @@ -72,7 +77,7 @@ class BleScanner @Inject constructor( "onScanResult(delay=${delay}ms, callbackType=$callbackType, result=$result)" } - trySend(resultFilter(setOf(result))) + trySend(filterResults(setOf(result))) } override fun onBatchScanResults(results: MutableList) { @@ -82,7 +87,7 @@ class BleScanner @Inject constructor( "onBatchScanResults(delay=${delay}ms, results=$results)" } - trySend(resultFilter(results)) + trySend(filterResults(results)) } override fun onScanFailed(errorCode: Int) { @@ -90,6 +95,37 @@ class BleScanner @Inject constructor( } } + val forwarderConsumer = if (disableDirectScanCallback) { + scanResultForwarder.results + .onEach { results -> trySend(filterResults(results)) } + .launchIn(this) + } else { + null + } + + val flushJob = if (!disableDirectScanCallback) { + launch { + log(TAG) { "Flush job launched" } + while (isActive) { + log(TAG, VERBOSE) { "Flushing scan results." } + // Can undercut the minimum setReportDelay(), e.g. 5000ms on a Pixel5@12 + adapter.bluetoothLeScanner.flushPendingScanResults(callback) + when (scannerMode) { + ScannerMode.LOW_POWER -> break + ScannerMode.BALANCED -> delay(2000) + ScannerMode.LOW_LATENCY -> delay(500) + } + } + } + } else { + null + } + + val filterList = when { + useOffloadedFiltering -> filters.toList() + else -> emptyList() + } + val scanSettings = ScanSettings.Builder().apply { setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) when (scannerMode) { @@ -122,38 +158,50 @@ class BleScanner @Inject constructor( setReportDelay(delay) }.build() - - val flushJob = launch { - log(TAG) { "Flush job launched" } - while (isActive) { - log(TAG, VERBOSE) { "Flushing scan results." } - // Can undercut the minimum setReportDelay(), e.g. 5000ms on a Pixel5@12 - adapter.bluetoothLeScanner.flushPendingScanResults(callback) - when (scannerMode) { - ScannerMode.LOW_POWER -> break - ScannerMode.BALANCED -> delay(2000) - ScannerMode.LOW_LATENCY -> delay(500) - } - } + if (disableDirectScanCallback) { + val callbackIntent = createStartIntent() + log(TAG) { "Intent callback: startScan(filters=$filters, settings=$scanSettings, callbackIntent=$callbackIntent)" } + scanner.startScan(filterList, scanSettings, callbackIntent) + } else { + log(TAG) { "Direct callback: startScan(filters=$filters, settings=$scanSettings, callback=$callback)" } + scanner.startScan(filterList, scanSettings, callback) } - log(TAG) { "startScan(filters=$filters, settings=$scanSettings, callback=$callback)" } - val filterList = when { - useOffloadedFiltering -> filters.toList() - else -> emptyList() - } - - scanner.startScan(filterList, scanSettings, callback) - awaitClose { - flushJob.cancel() - scanner.stopScan(callback) + forwarderConsumer?.cancel() + flushJob?.cancel() + if (disableDirectScanCallback) { + scanner.stopScan(createStopIntent()) + } else { + scanner.stopScan(callback) + } log(TAG) { "BleScanner stopped" } } } .map { fakeBleData.maybeAddfakeData(it) } + private val receiverIntent by lazy { + Intent(context, BleScanResultReceiver::class.java).apply { + action = BleScanResultReceiver.ACTION + } + } + + private fun createStartIntent(): PendingIntent = PendingIntent.getBroadcast( + context, + CALLBACK_INTENT_REQUESTCODE, + receiverIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_MUTABLE + ) + + private fun createStopIntent(): PendingIntent = PendingIntent.getBroadcast( + context, + 270, + receiverIntent, + PendingIntentCompat.FLAG_IMMUTABLE + ) + companion object { + private const val CALLBACK_INTENT_REQUESTCODE = 270 private val TAG = logTag("Bluetooth", "BleScanner") } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/common/notifications/PendingIntentCompat.kt b/app-common/src/main/java/eu/darken/capod/common/notifications/PendingIntentCompat.kt similarity index 70% rename from app/src/main/java/eu/darken/capod/common/notifications/PendingIntentCompat.kt rename to app-common/src/main/java/eu/darken/capod/common/notifications/PendingIntentCompat.kt index 773f5de4..a34c81ad 100644 --- a/app/src/main/java/eu/darken/capod/common/notifications/PendingIntentCompat.kt +++ b/app-common/src/main/java/eu/darken/capod/common/notifications/PendingIntentCompat.kt @@ -9,4 +9,9 @@ object PendingIntentCompat { } else { 0 } + val FLAG_MUTABLE: Int = if (hasApiLevel(31)) { + PendingIntent.FLAG_MUTABLE + } else { + 0 + } } \ No newline at end of file diff --git a/app-common/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt b/app-common/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt index 1508ecce..65fb36c2 100644 --- a/app-common/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt +++ b/app-common/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt @@ -33,9 +33,12 @@ class GeneralSettings @Inject constructor( val mainDeviceAddress = preferences.createFlowPreference("core.maindevice.address", null) val mainDeviceModel = preferences.createFlowPreference("core.maindevice.model", PodDevice.Model.UNKNOWN, moshi) - val isOffloadedFilteringDisabled = - preferences.createFlowPreference("core.compat.offloaded.filtering.disabled", false) + val isOffloadedFilteringDisabled = preferences.createFlowPreference( + "core.compat.offloaded.filtering.disabled", + false + ) val isOffloadedBatchingDisabled = preferences.createFlowPreference("core.compat.offloaded.batching.disabled", false) + val useIndirectScanResultCallback = preferences.createFlowPreference("core.compat.indirectcallback.enabled", false) override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper( monitorMode, @@ -45,6 +48,7 @@ class GeneralSettings @Inject constructor( mainDeviceAddress, isOffloadedFilteringDisabled, isOffloadedBatchingDisabled, + useIndirectScanResultCallback, debugSettings.isAutoReportingEnabled, ) } \ No newline at end of file diff --git a/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt b/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt index de2e039b..0dcf32d6 100644 --- a/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt +++ b/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt @@ -88,7 +88,8 @@ class PodMonitor @Inject constructor( val scannerMode: ScannerMode, val showUnfiltered: Boolean, val offloadedFilteringDisabled: Boolean, - val offloadedBatchingDisabled: Boolean + val offloadedBatchingDisabled: Boolean, + val disableDirectCallback: Boolean, ) private fun createBleScanner() = combine( @@ -96,12 +97,20 @@ class PodMonitor @Inject constructor( debugSettings.showUnfiltered.flow, generalSettings.isOffloadedBatchingDisabled.flow, generalSettings.isOffloadedFilteringDisabled.flow, - ) { scannermode, showUnfiltered, isOffloadedBatchingDisabled, isOffloadedFilteringDisabled -> + generalSettings.useIndirectScanResultCallback.flow, + ) { + scannermode, + showUnfiltered, + isOffloadedBatchingDisabled, + isOffloadedFilteringDisabled, + useIndirectScanResultCallback, + -> ScannerOptions( scannerMode = scannermode, showUnfiltered = showUnfiltered, offloadedFilteringDisabled = isOffloadedFilteringDisabled, offloadedBatchingDisabled = isOffloadedBatchingDisabled, + disableDirectCallback = useIndirectScanResultCallback, ) } .flatMapLatest { options -> @@ -116,8 +125,9 @@ class PodMonitor @Inject constructor( bleScanner.scan( filters = filters, scannerMode = options.scannerMode, - offloadFiltering = !options.offloadedFilteringDisabled, - offloadBatching = !options.offloadedBatchingDisabled + disableOffloadFiltering = options.offloadedFilteringDisabled, + disableOffloadBatching = options.offloadedBatchingDisabled, + disableDirectScanCallback = options.disableDirectCallback, ).map { preFilterAndMap(it) } } diff --git a/app-common/src/main/res/drawable/ic_strategy_24.xml b/app-common/src/main/res/drawable/ic_strategy_24.xml new file mode 100644 index 00000000..34863b67 --- /dev/null +++ b/app-common/src/main/res/drawable/ic_strategy_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/monitor/core/receiver/BluetoothEventReceiver.kt b/app/src/main/java/eu/darken/capod/monitor/core/receiver/BluetoothEventReceiver.kt index 629a41ac..1961c6de 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/receiver/BluetoothEventReceiver.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/receiver/BluetoothEventReceiver.kt @@ -25,9 +25,9 @@ class BluetoothEventReceiver : BroadcastReceiver() { @Inject @AppScope lateinit var appScope: CoroutineScope override fun onReceive(context: Context, intent: Intent) { - log { "onReceive($context, $intent)" } + log(TAG) { "onReceive($context, $intent)" } if (!EXPECTED_ACTIONS.contains(intent.action)) { - log(WARN) { "Unknown action: $intent.action" } + log(TAG, WARN) { "Unknown action: ${intent.action}" } return } @@ -41,7 +41,7 @@ class BluetoothEventReceiver : BroadcastReceiver() { val supportedFeatures = ContinuityProtocol.BLE_FEATURE_UUIDS.filter { bluetoothDevice.hasFeature(it) } if (supportedFeatures.isEmpty()) { - log { "Device has no features we support." } + log(TAG) { "Device has no features we support." } return } else { log { "Device has the following we features we support $supportedFeatures" } @@ -49,7 +49,7 @@ class BluetoothEventReceiver : BroadcastReceiver() { val pending = goAsync() appScope.launch { - log { "Starting monitor" } + log(TAG) { "Starting monitor" } monitorControl.startMonitor(bluetoothDevice, forceStart = false) pending.finish() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c03455cb..d98153ff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -100,4 +100,6 @@ darken A widget showing the last known device status. + Indirect data delivery + Use an alternative method to receive BLE data from the system (broadcast instead of callback). \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_general.xml b/app/src/main/res/xml/preferences_general.xml index 136e652e..0870e624 100644 --- a/app/src/main/res/xml/preferences_general.xml +++ b/app/src/main/res/xml/preferences_general.xml @@ -61,6 +61,12 @@ android:summary="@string/settings_compat_offloaded_batching_disabled_summary" android:title="@string/settings_compat_offloaded_batching_disabled_title" /> + +