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" />
+
+