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 a425804d..45048101 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 @@ -29,24 +29,40 @@ class BleScanner @Inject constructor( @SuppressLint("MissingPermission") fun scan( filters: Set, - scannerMode: ScannerMode, - compatMode: Boolean, - ): Flow> = callbackFlow { - log(TAG, VERBOSE) { "scan(filters=$filters, scannerMode=$scannerMode, compatMode=$compatMode)" } - if (compatMode) log(TAG, WARN) { "Using compatibilityMode!" } + scannerMode: ScannerMode = ScannerMode.BALANCED, + offloadFiltering: Boolean = true, + offloadBatching: Boolean = true, + ): Flow> = callbackFlow { + log(TAG) { "scan(filters=$filters, scannerMode=$scannerMode)" } val adapter = bluetoothManager.adapter ?: throw IllegalStateException("Bluetooth adapter unavailable") - val supportsOffloadFiltering = adapter.isOffloadedFilteringSupported.also { + val useOffloadedFiltering = adapter.isOffloadedFilteringSupported.also { log(TAG, if (it) DEBUG else WARN) { "isOffloadedFilteringSupported=$it" } - } && !compatMode + } && offloadFiltering + if (!offloadFiltering) log(TAG, WARN) { "Offloaded filtering is disabled!" } - val supportsOffloadBatching = adapter.isOffloadedScanBatchingSupported.also { + val useOffloadedBatching = adapter.isOffloadedScanBatchingSupported.also { log(TAG, if (it) DEBUG else WARN) { "isOffloadedScanBatchingSupported=$it" } - } && !compatMode + } && offloadBatching + if (!offloadBatching) log(TAG, WARN) { "Offloaded scan-batching is disabled!" } val scanner = bluetoothManager.scanner ?: throw IllegalStateException("BLE scanner unavailable") + val resultFilter: (Collection) -> Collection = { results -> + results + .filter { result -> + val passed = when { + useOffloadedFiltering -> true + filters.isEmpty() -> true + else -> filters.any { it.matches(result) } + } + if (!passed) log(TAG, VERBOSE) { "Manually filtered $result" } + passed + } + .map { BleScanResult.fromScanResult(it) } + } + val callback = object : ScanCallback() { var lastScanAt = System.currentTimeMillis() override fun onScanResult(callbackType: Int, result: ScanResult) { @@ -55,17 +71,8 @@ class BleScanner @Inject constructor( lastScanAt = System.currentTimeMillis() "onScanResult(delay=${delay}ms, callbackType=$callbackType, result=$result)" } - val toSend = if ( - supportsOffloadFiltering - || filters.isEmpty() - || filters.any { it.matchesSafe(result) } - ) { - listOf(BleScanResult.fromScanResult(result)) - } else { - log(TAG, VERBOSE) { "Manual filtering: No match for $result" } - emptyList() - } - trySend(toSend) + + trySend(resultFilter(setOf(result))) } override fun onBatchScanResults(results: MutableList) { @@ -75,18 +82,7 @@ class BleScanner @Inject constructor( "onBatchScanResults(delay=${delay}ms, results=$results)" } - val toSend = results - .filter { result -> - val passed = when { - supportsOffloadFiltering -> true - filters.isEmpty() -> true - else -> filters.any { it.matches(result) } - } - if (!passed) log(TAG, VERBOSE) { "Manually filtered $result" } - passed - } - .map { BleScanResult.fromScanResult(it) } - trySend(toSend) + trySend(resultFilter(results)) } override fun onScanFailed(errorCode: Int) { @@ -94,48 +90,60 @@ class BleScanner @Inject constructor( } } - - val settings = ScanSettings.Builder().apply { - setScanMode( - when (scannerMode) { - ScannerMode.LOW_POWER -> ScanSettings.SCAN_MODE_LOW_POWER - ScannerMode.BALANCED -> ScanSettings.SCAN_MODE_BALANCED - ScannerMode.LOW_LATENCY -> ScanSettings.SCAN_MODE_LOW_LATENCY + val scanSettings = ScanSettings.Builder().apply { + setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) + when (scannerMode) { + ScannerMode.LOW_POWER -> { + setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) + setMatchMode(ScanSettings.MATCH_MODE_STICKY) + setNumOfMatches(ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT) + } + ScannerMode.BALANCED -> { + setScanMode(ScanSettings.SCAN_MODE_BALANCED) + setMatchMode(ScanSettings.MATCH_MODE_STICKY) + setNumOfMatches(ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT) + } + ScannerMode.LOW_LATENCY -> { + setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) + setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT) } - ) - if (supportsOffloadBatching) { - setReportDelay( - when (scannerMode) { - ScannerMode.LOW_POWER -> 2000L - ScannerMode.BALANCED -> 1000L - ScannerMode.LOW_LATENCY -> 500L - } - ) } + + val delay = if (useOffloadedBatching) { + when (scannerMode) { + ScannerMode.LOW_POWER -> 2000L + ScannerMode.BALANCED -> 1000L + ScannerMode.LOW_LATENCY -> 500L + } + } else { + 0L // Anything > 0 enables batching + } + setReportDelay(delay) }.build() - log(TAG, VERBOSE) { "Settings created for offloaded filtering: $settings" } val flushJob = launch { log(TAG) { "Flush job launched" } while (isActive) { - // Can undercut the minimum setReportDelay(), e.g. 5000ms on a Pixel5@12 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(1000) + ScannerMode.BALANCED -> delay(2000) ScannerMode.LOW_LATENCY -> delay(500) } } } - scanner.startScan( - if (supportsOffloadFiltering) filters.toList() else listOf(ScanFilter.Builder().build()), - settings, - callback - ) - log(TAG) { "BleScanner started (filters=$filters, settings=$settings)" } + 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() diff --git a/app-common/src/main/java/eu/darken/capod/common/bluetooth/FakeBleData.kt b/app-common/src/main/java/eu/darken/capod/common/bluetooth/FakeBleData.kt index 664170d4..00eef122 100644 --- a/app-common/src/main/java/eu/darken/capod/common/bluetooth/FakeBleData.kt +++ b/app-common/src/main/java/eu/darken/capod/common/bluetooth/FakeBleData.kt @@ -12,7 +12,7 @@ class FakeBleData @Inject constructor( private val debugSettings: DebugSettings, ) { - fun maybeAddfakeData(originals: List): List { + fun maybeAddfakeData(originals: Collection): Collection { if (!debugSettings.showFakeData.value) return originals return originals + getFakeData() } 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 e33118fe..1508ecce 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 @@ -23,51 +23,28 @@ class GeneralSettings @Inject constructor( override val preferences: SharedPreferences = context.getSharedPreferences("settings_general", Context.MODE_PRIVATE) - val monitorMode = preferences.createFlowPreference( - "core.monitor.mode", - MonitorMode.AUTOMATIC, - moshi - ) + val monitorMode = preferences.createFlowPreference("core.monitor.mode", MonitorMode.AUTOMATIC, moshi) + val scannerMode = preferences.createFlowPreference("core.scanner.mode", ScannerMode.BALANCED, moshi) - val scannerMode = preferences.createFlowPreference( - "core.scanner.mode", - ScannerMode.LOW_LATENCY, - moshi - ) + val showAll = preferences.createFlowPreference("core.showall.enabled", false) - val compatibilityMode = preferences.createFlowPreference( - "core.compatibility.enabled", - false - ) + val minimumSignalQuality = preferences.createFlowPreference("core.signal.minimum", 0.25f) - val showAll = preferences.createFlowPreference( - "core.showall.enabled", - false - ) + val mainDeviceAddress = preferences.createFlowPreference("core.maindevice.address", null) + val mainDeviceModel = preferences.createFlowPreference("core.maindevice.model", PodDevice.Model.UNKNOWN, moshi) - val minimumSignalQuality = preferences.createFlowPreference( - "core.signal.minimum", - 0.25f - ) - - 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 isOffloadedBatchingDisabled = preferences.createFlowPreference("core.compat.offloaded.batching.disabled", false) override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper( monitorMode, scannerMode, - compatibilityMode, showAll, minimumSignalQuality, mainDeviceAddress, + isOffloadedFilteringDisabled, + isOffloadedBatchingDisabled, 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 b2d5797d..de2e039b 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 @@ -4,6 +4,7 @@ import android.bluetooth.le.ScanFilter import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.common.bluetooth.BleScanner import eu.darken.capod.common.bluetooth.BluetoothManager2 +import eu.darken.capod.common.bluetooth.ScannerMode import eu.darken.capod.common.coroutine.AppScope import eu.darken.capod.common.debug.autoreport.DebugSettings import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE @@ -83,26 +84,40 @@ class PodMonitor @Inject constructor( .setupCommonEventHandlers(TAG) { "mainDevice" } .replayingShare(appScope) + private data class ScannerOptions( + val scannerMode: ScannerMode, + val showUnfiltered: Boolean, + val offloadedFilteringDisabled: Boolean, + val offloadedBatchingDisabled: Boolean + ) + private fun createBleScanner() = combine( generalSettings.scannerMode.flow, - generalSettings.compatibilityMode.flow, - debugSettings.showUnfiltered.flow - ) { scannerMode, compatMode, unfiltered -> - Triple(scannerMode, compatMode, unfiltered) + debugSettings.showUnfiltered.flow, + generalSettings.isOffloadedBatchingDisabled.flow, + generalSettings.isOffloadedFilteringDisabled.flow, + ) { scannermode, showUnfiltered, isOffloadedBatchingDisabled, isOffloadedFilteringDisabled -> + ScannerOptions( + scannerMode = scannermode, + showUnfiltered = showUnfiltered, + offloadedFilteringDisabled = isOffloadedFilteringDisabled, + offloadedBatchingDisabled = isOffloadedBatchingDisabled, + ) } - .flatMapLatest { (mode, compat, unfiltered) -> + .flatMapLatest { options -> val filters = when { - unfiltered -> { + options.showUnfiltered -> { log(TAG, WARN) { "Using unfiltered scan mode" } - setOf(getUnfilteredFilter()) + setOf(ScanFilter.Builder().build()) } else -> ProximityPairing.getBleScanFilter() } bleScanner.scan( filters = filters, - scannerMode = mode, - compatMode = compat, + scannerMode = options.scannerMode, + offloadFiltering = !options.offloadedFilteringDisabled, + offloadBatching = !options.offloadedBatchingDisabled ).map { preFilterAndMap(it) } } @@ -134,7 +149,7 @@ class PodMonitor @Inject constructor( return pods } - private suspend fun preFilterAndMap(rawResults: List): List = rawResults + private suspend fun preFilterAndMap(rawResults: Collection): List = rawResults .groupBy { it.address } .values .map { sameAdrDevs -> @@ -187,10 +202,6 @@ class PodMonitor @Inject constructor( .also { log(TAG) { "Cached mainDevice is $it" } } } - private fun getUnfilteredFilter(): ScanFilter { - return ScanFilter.Builder().build() - } - companion object { private val TAG = logTag("Monitor", "PodMonitor") } diff --git a/app-common/src/main/res/drawable/ic_chip_24.xml b/app-common/src/main/res/drawable/ic_chip_24.xml new file mode 100644 index 00000000..0dac0f9e --- /dev/null +++ b/app-common/src/main/res/drawable/ic_chip_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app-common/src/main/res/drawable/ic_filter_cog_outline_24.xml b/app-common/src/main/res/drawable/ic_filter_cog_outline_24.xml new file mode 100644 index 00000000..0892ecda --- /dev/null +++ b/app-common/src/main/res/drawable/ic_filter_cog_outline_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app-common/src/main/res/drawable/ic_format_list_group_24.xml b/app-common/src/main/res/drawable/ic_format_list_group_24.xml new file mode 100644 index 00000000..9a2d4ffd --- /dev/null +++ b/app-common/src/main/res/drawable/ic_format_list_group_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a2566f0..c03455cb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,6 +33,14 @@ Reactions React to events and behaviors. Your device + + Compatibility options + Don\'t touch if everything works ;) + Disable hardware filtering + Don\'t delegate data filtering to the system, instead get all data and filter within the app. + Disable hardware batching + Don\'t let the system group collected BLE data before forwarding it to us. + Your device address The address of your paired device. The app uses this to determine when it is connected to your phone. None @@ -43,6 +51,7 @@ One pod mode Wearing both pods is not required, wearing a single pod is sufficient to trigger reactions. + Device status Size diff --git a/app/src/main/res/xml/preferences_general.xml b/app/src/main/res/xml/preferences_general.xml index 019a1e68..136e652e 100644 --- a/app/src/main/res/xml/preferences_general.xml +++ b/app/src/main/res/xml/preferences_general.xml @@ -14,12 +14,6 @@ android:summary="@string/settings_scanner_mode_description" android:title="@string/settings_scanner_mode_label" /> - - + + + + + + + +