From f9d39a6319f228a6ea7e896e17ec7534e1c14876 Mon Sep 17 00:00:00 2001 From: darken Date: Thu, 6 Jan 2022 11:59:30 +0100 Subject: [PATCH] Add option to show fake devices --- .../capod/common/bluetooth/BleScanResult.kt | 38 ++++++++++++ .../capod/common/bluetooth/BleScanner.kt | 61 ++++++++++++++++++- .../common/debug/autoreport/DebugSettings.kt | 14 ++++- .../darken/capod/main/core/GeneralSettings.kt | 8 +-- .../main/ui/overview/OverviewFragment.kt | 4 -- .../cards/pods/UnknownPodDeviceCardVH.kt | 2 + .../general/debug/DebugSettingsFragment.kt | 40 ++++++++++++ .../general/debug/DebugSettingsFragmentVM.kt | 32 ++++++++++ .../eu/darken/capod/pods/core/PodDevice.kt | 4 +- .../eu/darken/capod/pods/core/PodFactory.kt | 4 +- .../capod/pods/core/apple/AppleFactory.kt | 28 +++------ .../darken/capod/pods/core/apple/ApplePods.kt | 2 +- .../pods/core/apple/UnknownAppleDevice.kt | 6 +- .../pods/core/apple/airpods/AirPodsGen1.kt | 4 +- .../pods/core/apple/airpods/AirPodsGen2.kt | 4 +- .../pods/core/apple/airpods/AirPodsMax.kt | 6 +- .../pods/core/apple/airpods/AirPodsPro.kt | 6 +- .../capod/pods/core/apple/beats/BeatsFlex.kt | 6 +- .../capod/pods/core/apple/beats/BeatsSolo3.kt | 6 +- .../pods/core/apple/beats/BeatsStudio3.kt | 6 +- .../capod/pods/core/apple/beats/BeatsX.kt | 6 +- .../pods/core/apple/beats/PowerBeats3.kt | 6 +- .../pods/core/apple/beats/PowerBeatsPro.kt | 6 +- .../core/apple/protocol/ContinuityProtocol.kt | 6 +- .../drawable/ic_baseline_data_array_24.xml | 13 ++++ .../drawable/ic_baseline_text_snippet_24.xml | 11 ++++ app/src/main/res/menu/main.xml | 10 +-- app/src/main/res/values/strings.xml | 4 ++ app/src/main/res/xml/preferences_debug.xml | 21 +++++++ app/src/main/res/xml/preferences_general.xml | 9 +-- .../capod/pods/core/apple/BaseAirPodsTest.kt | 33 +++++----- .../pods/core/apple/SingleApplePodsTest.kt | 2 - .../pods/core/apple/airpods/AirPodsMaxTest.kt | 4 -- .../pods/core/apple/airpods/AirPodsProTest.kt | 2 +- 34 files changed, 307 insertions(+), 107 deletions(-) create mode 100644 app/src/main/java/eu/darken/capod/common/bluetooth/BleScanResult.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsFragment.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsFragmentVM.kt create mode 100644 app/src/main/res/drawable/ic_baseline_data_array_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_text_snippet_24.xml create mode 100644 app/src/main/res/xml/preferences_debug.xml diff --git a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanResult.kt b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanResult.kt new file mode 100644 index 00000000..4bacb273 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanResult.kt @@ -0,0 +1,38 @@ +package eu.darken.capod.common.bluetooth + +import android.bluetooth.le.ScanResult +import android.os.Parcelable +import androidx.core.util.forEach +import kotlinx.parcelize.Parcelize + +@Parcelize +data class BleScanResult( + val address: String, + val rssi: Int, + val generatedAtNanos: Long, + val manufacturerSpecificData: Map +) : Parcelable { + + fun getManufacturerSpecificData(id: Int): ByteArray? = manufacturerSpecificData[id] + + override fun toString(): String { + val sb = StringBuilder() + manufacturerSpecificData.forEach { (key, value) -> + sb.append("$key: ${value.joinToString(separator = " ") { String.format("%02X", it) }}") + } + return "BleScanResult($rssi, $address, $generatedAtNanos, $sb" + } + + companion object { + fun fromScanResult(scanResult: ScanResult) = BleScanResult( + address = scanResult.device.address, + rssi = scanResult.rssi, + generatedAtNanos = scanResult.timestampNanos, + manufacturerSpecificData = mutableMapOf().apply { + scanResult.scanRecord?.manufacturerSpecificData?.forEach { key, value -> + this[key] = value + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt index ed9d0ec4..b2d7e6aa 100644 --- a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt +++ b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt @@ -6,6 +6,8 @@ import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanSettings import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext +import eu.darken.capod.common.SystemClockWrap +import eu.darken.capod.common.debug.autoreport.DebugSettings 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 @@ -13,6 +15,7 @@ import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.map import javax.inject.Inject import javax.inject.Singleton @@ -20,24 +23,25 @@ import javax.inject.Singleton class BleScanner @Inject constructor( @ApplicationContext private val context: Context, private val bluetoothManager: BluetoothManager2, + private val debugSettings: DebugSettings, ) { fun scan( filter: Set = ProximityPairing.getBleScanFilter(), mode: Int = ScanSettings.SCAN_MODE_BALANCED, - ): Flow> = callbackFlow { + ): Flow> = callbackFlow { val adapter = bluetoothManager.adapter val scanner = bluetoothManager.scanner val callback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { log(TAG, VERBOSE) { "onScanResult(callbackType=$callbackType, result=$result)" } - trySend(listOf(result)) + trySend(listOf(BleScanResult.fromScanResult(result))) } override fun onBatchScanResults(results: MutableList) { log(TAG, VERBOSE) { "onBatchScanResults(results=$results)" } - trySend(results) + trySend(results.map { BleScanResult.fromScanResult(it) }) } override fun onScanFailed(errorCode: Int) { @@ -68,7 +72,58 @@ class BleScanner @Inject constructor( scanner.stopScan(callback) } } + .map { origs -> + val fakeDevices = mutableListOf() + if (debugSettings.showFakeData.value) { + // AirPods Pro + BleScanResult( + address = "78:73:AF:B4:85:5E", + rssi = -48, + generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100, + manufacturerSpecificData = mapOf(76 to "07 19 01 0E 20 75 AA B6 31 00 00 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray()) + ).run { fakeDevices.add(this) } + // AirPods Gen1 + BleScanResult( + address = "4E:9E:D1:49:D2:6D", + rssi = -55, + generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200, + manufacturerSpecificData = mapOf(76 to "07 19 01 02 20 55 AF 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88".hexToByteArray()) + ).run { fakeDevices.add(this) } + // AirPods Max + BleScanResult( + address = "7E:E5:C7:65:D2:B5", + rssi = -57, + generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300, + manufacturerSpecificData = mapOf(76 to "07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E".hexToByteArray()) + ).run { fakeDevices.add(this) } + // BeatsFlex + BleScanResult( + address = "5E:9E:D1:49:D2:6D", + rssi = -59, + generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400, + manufacturerSpecificData = mapOf(76 to "07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray()) + ).run { fakeDevices.add(this) } + // Unknown Device + BleScanResult( + address = "6E:9E:D1:49:D2:6D", + rssi = -60, + generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500, + manufacturerSpecificData = mapOf(76 to "07 19 01 FF 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray()) + ).run { fakeDevices.add(this) } + } + origs + fakeDevices + } + + + fun String.hexToByteArray(): ByteArray { + val trimmed = this + .replace(" ", "") + .replace(">", "") + .replace("<", "") + require(trimmed.length % 2 == 0) { "Not a HEX string" } + return trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray() + } companion object { private val TAG = logTag("Bluetooth", "BleScanner") diff --git a/app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt b/app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt index e160e8d1..b2da0e3a 100644 --- a/app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt +++ b/app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt @@ -2,7 +2,10 @@ package eu.darken.capod.common.debug.autoreport import android.content.Context import android.content.SharedPreferences +import androidx.preference.PreferenceDataStore import dagger.hilt.android.qualifiers.ApplicationContext +import eu.darken.androidstarter.common.preferences.Settings +import eu.darken.capod.common.preferences.PreferenceStoreMapper import eu.darken.capod.common.preferences.createFlowPreference import javax.inject.Inject import javax.inject.Singleton @@ -10,12 +13,19 @@ import javax.inject.Singleton @Singleton class DebugSettings @Inject constructor( @ApplicationContext private val context: Context, -) { +) : Settings() { - private val preferences: SharedPreferences = context.getSharedPreferences("settings_debug", Context.MODE_PRIVATE) + override val preferences: SharedPreferences = context.getSharedPreferences("settings_debug", Context.MODE_PRIVATE) val isAutoReportEnabled = preferences.createFlowPreference("debug.bugreport.automatic.enabled", true) val isDebugModeEnabled = preferences.createFlowPreference("debug.mode.enabled", false) + val showFakeData = preferences.createFlowPreference("debug.fakedata.enabled", false) + + override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper( + isDebugModeEnabled, + showFakeData, + ) + } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt b/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt index 1e441cc1..4b647047 100644 --- a/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt +++ b/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt @@ -7,7 +7,6 @@ import com.squareup.moshi.Moshi import dagger.hilt.android.qualifiers.ApplicationContext import eu.darken.androidstarter.common.preferences.Settings import eu.darken.capod.common.debug.autoreport.DebugSettings -import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.preferences.PreferenceStoreMapper import eu.darken.capod.common.preferences.createFlowPreference import javax.inject.Inject @@ -55,11 +54,6 @@ class GeneralSettings @Inject constructor( autoPause, autoPlay, showAll, - debugSettings.isDebugModeEnabled, - debugSettings.isAutoReportEnabled + debugSettings.isAutoReportEnabled, ) - - companion object { - internal val TAG = logTag("Core", "Settings") - } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragment.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragment.kt index be0a2cd9..ff521f6a 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragment.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragment.kt @@ -45,10 +45,6 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) { subtitle = BuildConfigWrap.VERSION_DESCRIPTION setOnMenuItemClickListener { when (it.itemId) { - R.id.menu_item_debuglog -> { - vm.toggleDebugLog() - true - } R.id.menu_item_settings -> { vm.goToSettings() true diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt index d70e3862..645c1c9c 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt @@ -32,6 +32,8 @@ class UnknownPodDeviceCardVH(parent: ViewGroup) : RelativeDateTimeFormatter.RelativeUnit.SECONDS ) + reception.text = device.getSignalQuality(context) + rawdata.text = device.rawDataHex } diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsFragment.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsFragment.kt new file mode 100644 index 00000000..4576ac1c --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsFragment.kt @@ -0,0 +1,40 @@ +package eu.darken.capod.main.ui.settings.general.debug + +import android.os.Bundle +import android.view.View +import androidx.annotation.Keep +import androidx.fragment.app.viewModels +import androidx.preference.Preference +import dagger.hilt.android.AndroidEntryPoint +import eu.darken.capod.R +import eu.darken.capod.common.debug.autoreport.DebugSettings +import eu.darken.capod.common.observe2 +import eu.darken.capod.common.uix.PreferenceFragment2 +import javax.inject.Inject + +@Keep +@AndroidEntryPoint +class DebugSettingsFragment : PreferenceFragment2() { + + private val vm: DebugSettingsFragmentVM by viewModels() + + @Inject lateinit var debugSettings: DebugSettings + + override val settings: DebugSettings + get() = debugSettings + + override val preferenceFile: Int = R.xml.preferences_debug + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val logPref = findPreference("debug.log.record")!! + vm.state.observe2(this) { + logPref.summary = it.currentLogPath?.path + } + logPref.setOnPreferenceClickListener { + vm.toggleRecorder() + true + } + super.onViewCreated(view, savedInstanceState) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsFragmentVM.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsFragmentVM.kt new file mode 100644 index 00000000..caa11be5 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsFragmentVM.kt @@ -0,0 +1,32 @@ +package eu.darken.capod.main.ui.settings.general.debug + +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import eu.darken.capod.common.coroutine.DispatcherProvider +import eu.darken.capod.common.debug.logging.logTag +import eu.darken.capod.common.debug.recording.core.RecorderModule +import eu.darken.capod.common.uix.ViewModel3 +import kotlinx.coroutines.flow.first +import javax.inject.Inject + +@HiltViewModel +class DebugSettingsFragmentVM @Inject constructor( + private val handle: SavedStateHandle, + dispatcherProvider: DispatcherProvider, + private val recorderModule: RecorderModule, +) : ViewModel3(dispatcherProvider) { + + val state = recorderModule.state.asLiveData2() + + fun toggleRecorder() = launch { + if (recorderModule.state.first().isRecording) { + recorderModule.stopRecorder() + } else { + recorderModule.startRecorder() + } + } + + companion object { + private val TAG = logTag("Settings", "Debug", "VM") + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt b/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt index 56c3c370..fed20681 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt @@ -1,9 +1,9 @@ package eu.darken.capod.pods.core -import android.bluetooth.le.ScanResult import android.content.Context import androidx.annotation.DrawableRes import eu.darken.capod.R +import eu.darken.capod.common.bluetooth.BleScanResult import java.time.Instant import java.util.* import kotlin.math.abs @@ -14,7 +14,7 @@ interface PodDevice { val lastSeenAt: Instant - val scanResult: ScanResult + val scanResult: BleScanResult val rssi: Int get() = scanResult.rssi diff --git a/app/src/main/java/eu/darken/capod/pods/core/PodFactory.kt b/app/src/main/java/eu/darken/capod/pods/core/PodFactory.kt index 45fc62e1..edb096ca 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/PodFactory.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/PodFactory.kt @@ -1,7 +1,7 @@ package eu.darken.capod.pods.core -import android.bluetooth.le.ScanResult import dagger.Reusable +import eu.darken.capod.common.bluetooth.BleScanResult 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 @@ -14,7 +14,7 @@ class PodFactory @Inject constructor( private val appleFactory: AppleFactory ) { - suspend fun createPod(scanResult: ScanResult): PodDevice? { + suspend fun createPod(scanResult: BleScanResult): PodDevice? { log(TAG, VERBOSE) { "Trying to create Pod for $scanResult" } val pod = appleFactory.create(scanResult) diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt index 478e37ae..726c402c 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt @@ -1,7 +1,7 @@ package eu.darken.capod.pods.core.apple -import android.bluetooth.le.ScanResult import eu.darken.capod.common.SystemClockWrap +import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.common.debug.Bugs import eu.darken.capod.common.debug.logging.Logging.Priority.* import eu.darken.capod.common.debug.logging.asLog @@ -28,17 +28,17 @@ class AppleFactory @Inject constructor( data class KnownDevice( val identifier: PodDevice.Id, - val scanResult: ScanResult, + val scanResult: BleScanResult, val message: ProximityPairing.Message, ) { val address: String - get() = scanResult.device.address + get() = scanResult.address val rssi: Int get() = scanResult.rssi val timestampNanos: Duration - get() = Duration.ofNanos(scanResult.timestampNanos) + get() = Duration.ofNanos(scanResult.generatedAtNanos) fun isOlderThan(age: Duration): Boolean { val now = Duration.ofNanos(SystemClockWrap.elapsedRealtimeNanos) @@ -55,7 +55,7 @@ class AppleFactory @Inject constructor( private val cachedValues = mutableMapOf() - private suspend fun getMessage(scanResult: ScanResult): ProximityPairing.Message? { + private suspend fun getMessage(scanResult: BleScanResult): ProximityPairing.Message? { val messages = try { continuityProtocolDecoder.decode(scanResult) } catch (e: Exception) { @@ -80,8 +80,8 @@ class AppleFactory @Inject constructor( return proximityMessage } - private suspend fun recognizeDevice(scanResult: ScanResult, message: ProximityPairing.Message): PodDevice.Id { - val address = scanResult.device.address + private suspend fun recognizeDevice(scanResult: BleScanResult, message: ProximityPairing.Message): PodDevice.Id { + val address = scanResult.address var identifier: PodDevice.Id? = null @@ -147,26 +147,18 @@ class AppleFactory @Inject constructor( return identifier!! } - suspend fun create(scanResult: ScanResult): PodDevice? = lock.withLock { + suspend fun create(scanResult: BleScanResult): PodDevice? = lock.withLock { val pm = getMessage(scanResult) ?: return@withLock null val identifier = recognizeDevice(scanResult, pm) - log(TAG, INFO) { - val data = scanResult.scanRecord!!.getManufacturerSpecificData( - ContinuityProtocol.APPLE_COMPANY_IDENTIFIER - )!! - val dataHex = data.joinToString(separator = " ") { - String.format("%02X", it) - } - "Decoding (MAC=${scanResult.device.address}, nanos=${scanResult.timestampNanos}, rssi=${scanResult.rssi}): $dataHex" - } + log(TAG, INFO) { "Decoding $scanResult" } return createSpecificDevice(scanResult, pm, identifier) } private fun createSpecificDevice( - scanResult: ScanResult, + scanResult: BleScanResult, pm: ProximityPairing.Message, identifier: PodDevice.Id ): ApplePods { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt index 491d91cd..a045aa6d 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt @@ -9,7 +9,7 @@ interface ApplePods : PodDevice { val proximityMessage: ProximityPairing.Message override val rawData: ByteArray - get() = scanResult.scanRecord!!.getManufacturerSpecificData(ContinuityProtocol.APPLE_COMPANY_IDENTIFIER)!! + get() = scanResult.getManufacturerSpecificData(ContinuityProtocol.APPLE_COMPANY_IDENTIFIER)!! // We start counting at the airpods prefix byte val rawPrefix: UByte diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt index 7b8de5e5..88b884fa 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt @@ -1,16 +1,16 @@ package eu.darken.capod.pods.core.apple -import android.bluetooth.le.ScanResult import android.content.Context import eu.darken.capod.R +import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant -data class UnknownAppleDevice constructor( +data class UnknownAppleDevice( override val identifier: PodDevice.Id = PodDevice.Id(), override val lastSeenAt: Instant = Instant.now(), - override val scanResult: ScanResult, + override val scanResult: BleScanResult, override val proximityMessage: ProximityPairing.Message ) : ApplePods { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt index a45cbbb6..6f16b2b6 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt @@ -1,8 +1,8 @@ package eu.darken.capod.pods.core.apple.airpods -import android.bluetooth.le.ScanResult import android.content.Context import eu.darken.capod.R +import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing @@ -11,7 +11,7 @@ import java.time.Instant data class AirPodsGen1 constructor( override val identifier: PodDevice.Id = PodDevice.Id(), override val lastSeenAt: Instant = Instant.now(), - override val scanResult: ScanResult, + override val scanResult: BleScanResult, override val proximityMessage: ProximityPairing.Message, private val cachedBatteryPercentage: Float? ) : DualApplePods { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt index 4dc46098..fc1608d6 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt @@ -1,8 +1,8 @@ package eu.darken.capod.pods.core.apple.airpods -import android.bluetooth.le.ScanResult import android.content.Context import eu.darken.capod.R +import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing @@ -11,7 +11,7 @@ import java.time.Instant data class AirPodsGen2 constructor( override val identifier: PodDevice.Id = PodDevice.Id(), override val lastSeenAt: Instant = Instant.now(), - override val scanResult: ScanResult, + override val scanResult: BleScanResult, override val proximityMessage: ProximityPairing.Message, private val cachedBatteryPercentage: Float?, ) : DualApplePods { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt index fc2c540e..3dc22d64 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt @@ -1,17 +1,17 @@ package eu.darken.capod.pods.core.apple.airpods -import android.bluetooth.le.ScanResult import android.content.Context import eu.darken.capod.R +import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.SingleApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant -data class AirPodsMax constructor( +data class AirPodsMax( override val identifier: PodDevice.Id = PodDevice.Id(), override val lastSeenAt: Instant = Instant.now(), - override val scanResult: ScanResult, + override val scanResult: BleScanResult, override val proximityMessage: ProximityPairing.Message, ) : SingleApplePods { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt index 2e318fdb..4540b213 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt @@ -1,17 +1,17 @@ package eu.darken.capod.pods.core.apple.airpods -import android.bluetooth.le.ScanResult import android.content.Context import eu.darken.capod.R +import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant -data class AirPodsPro constructor( +data class AirPodsPro( override val identifier: PodDevice.Id = PodDevice.Id(), override val lastSeenAt: Instant = Instant.now(), - override val scanResult: ScanResult, + override val scanResult: BleScanResult, override val proximityMessage: ProximityPairing.Message, private val cachedBatteryPercentage: Float?, ) : DualApplePods { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt index 82a9c235..d8a18fd3 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt @@ -1,17 +1,17 @@ package eu.darken.capod.pods.core.apple.beats -import android.bluetooth.le.ScanResult import android.content.Context import eu.darken.capod.R +import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant -data class BeatsFlex constructor( +data class BeatsFlex( override val identifier: PodDevice.Id = PodDevice.Id(), override val lastSeenAt: Instant = Instant.now(), - override val scanResult: ScanResult, + override val scanResult: BleScanResult, override val proximityMessage: ProximityPairing.Message ) : BasicSingleApplePods { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt index 1dfc8ce1..fb646a45 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt @@ -1,17 +1,17 @@ package eu.darken.capod.pods.core.apple.beats -import android.bluetooth.le.ScanResult import android.content.Context import eu.darken.capod.R +import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant -data class BeatsSolo3 constructor( +data class BeatsSolo3( override val identifier: PodDevice.Id = PodDevice.Id(), override val lastSeenAt: Instant = Instant.now(), - override val scanResult: ScanResult, + override val scanResult: BleScanResult, override val proximityMessage: ProximityPairing.Message ) : BasicSingleApplePods { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt index 6b84daed..2e86dd85 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt @@ -1,17 +1,17 @@ package eu.darken.capod.pods.core.apple.beats -import android.bluetooth.le.ScanResult import android.content.Context import eu.darken.capod.R +import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant -data class BeatsStudio3 constructor( +data class BeatsStudio3( override val identifier: PodDevice.Id = PodDevice.Id(), override val lastSeenAt: Instant = Instant.now(), - override val scanResult: ScanResult, + override val scanResult: BleScanResult, override val proximityMessage: ProximityPairing.Message ) : BasicSingleApplePods { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt index 495e83ae..ad0dce34 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt @@ -1,17 +1,17 @@ package eu.darken.capod.pods.core.apple.beats -import android.bluetooth.le.ScanResult import android.content.Context import eu.darken.capod.R +import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant -data class BeatsX constructor( +data class BeatsX( override val identifier: PodDevice.Id = PodDevice.Id(), override val lastSeenAt: Instant = Instant.now(), - override val scanResult: ScanResult, + override val scanResult: BleScanResult, override val proximityMessage: ProximityPairing.Message ) : BasicSingleApplePods { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt index c225fd37..3d1c2f3c 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt @@ -1,17 +1,17 @@ package eu.darken.capod.pods.core.apple.beats -import android.bluetooth.le.ScanResult import android.content.Context import eu.darken.capod.R +import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant -data class PowerBeats3 constructor( +data class PowerBeats3( override val identifier: PodDevice.Id = PodDevice.Id(), override val lastSeenAt: Instant = Instant.now(), - override val scanResult: ScanResult, + override val scanResult: BleScanResult, override val proximityMessage: ProximityPairing.Message ) : BasicSingleApplePods { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt index f1980f19..3d939f17 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt @@ -1,17 +1,17 @@ package eu.darken.capod.pods.core.apple.beats -import android.bluetooth.le.ScanResult import android.content.Context import eu.darken.capod.R +import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant -data class PowerBeatsPro constructor( +data class PowerBeatsPro( override val identifier: PodDevice.Id = PodDevice.Id(), override val lastSeenAt: Instant = Instant.now(), - override val scanResult: ScanResult, + override val scanResult: BleScanResult, override val proximityMessage: ProximityPairing.Message, private val cachedBatteryPercentage: Float?, ) : DualApplePods { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/protocol/ContinuityProtocol.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/protocol/ContinuityProtocol.kt index c0a3e7a3..4c2b0cc5 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/protocol/ContinuityProtocol.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/protocol/ContinuityProtocol.kt @@ -1,8 +1,8 @@ package eu.darken.capod.pods.core.apple.protocol -import android.bluetooth.le.ScanResult import android.os.ParcelUuid import dagger.Reusable +import eu.darken.capod.common.bluetooth.BleScanResult 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 @@ -29,8 +29,8 @@ object ContinuityProtocol { @Reusable class Decoder @Inject constructor() { - fun decode(scanResult: ScanResult): List = scanResult.scanRecord - ?.getManufacturerSpecificData(APPLE_COMPANY_IDENTIFIER) + fun decode(scanResult: BleScanResult): List = scanResult + .getManufacturerSpecificData(APPLE_COMPANY_IDENTIFIER) ?.let { data -> val messages = mutableListOf() diff --git a/app/src/main/res/drawable/ic_baseline_data_array_24.xml b/app/src/main/res/drawable/ic_baseline_data_array_24.xml new file mode 100644 index 00000000..18741804 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_data_array_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_text_snippet_24.xml b/app/src/main/res/drawable/ic_baseline_text_snippet_24.xml new file mode 100644 index 00000000..56299561 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_text_snippet_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml index 263b5b20..580eaf59 100644 --- a/app/src/main/res/menu/main.xml +++ b/app/src/main/res/menu/main.xml @@ -1,9 +1,9 @@ - + - + android:icon="@drawable/ic_baseline_settings_24" + android:title="@string/settings_general_label" + app:showAsAction="always" /> \ 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 80ff9549..e1fc60ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -103,4 +103,8 @@ Headphones: %1$s ACCESS_BACKGROUND_LOCATION Allows an app to access location in the background. + Fake data + Show fake data, i.e. simulate device that don\'t exist. + Debug settings + Additional settings to help troubleshoot issues with the app. \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_debug.xml b/app/src/main/res/xml/preferences_debug.xml new file mode 100644 index 00000000..bc1e8527 --- /dev/null +++ b/app/src/main/res/xml/preferences_debug.xml @@ -0,0 +1,21 @@ + + + + + + + + + + \ 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 84f487f1..516e4104 100644 --- a/app/src/main/res/xml/preferences_general.xml +++ b/app/src/main/res/xml/preferences_general.xml @@ -39,11 +39,12 @@ android:summary="@string/settings_debug_autoreports_description" android:title="@string/settings_debug_autoreports_label" /> - + android:key="debug.settings" + android:fragment="eu.darken.capod.main.ui.settings.general.debug.DebugSettingsFragment" + android:summary="@string/settings_debug_description" + android:title="@string/settings_debug_label" /> diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/BaseAirPodsTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/BaseAirPodsTest.kt index d16904af..bc309026 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/apple/BaseAirPodsTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/apple/BaseAirPodsTest.kt @@ -1,24 +1,24 @@ package eu.darken.capod.pods.core.apple -import android.bluetooth.BluetoothDevice -import android.bluetooth.le.ScanRecord -import android.bluetooth.le.ScanResult import eu.darken.capod.common.SystemClockWrap +import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import io.mockk.MockKAnnotations import io.mockk.every -import io.mockk.impl.annotations.MockK import io.mockk.mockkObject import org.junit.jupiter.api.BeforeEach import testhelper.BaseTest abstract class BaseAirPodsTest : BaseTest() { - @MockK lateinit var scanResult: ScanResult - @MockK lateinit var scanRecord: ScanRecord - @MockK lateinit var device: BluetoothDevice + private val baseBleScanResult = BleScanResult( + address = "77:49:4C:D8:25:0C", + rssi = -66, + generatedAtNanos = 136136027721826, + manufacturerSpecificData = emptyMap() + ) val factory = AppleFactory( proximityPairingDecoder = ProximityPairing.Decoder(), @@ -28,11 +28,6 @@ abstract class BaseAirPodsTest : BaseTest() { @BeforeEach fun setup() { MockKAnnotations.init(this) - every { scanResult.scanRecord } returns scanRecord - every { scanResult.rssi } returns -66 - every { scanResult.timestampNanos } returns 136136027721826 - every { scanResult.device } returns device - every { device.address } returns "77:49:4C:D8:25:0C" mockkObject(SystemClockWrap) every { SystemClockWrap.elapsedRealtimeNanos } returns 1000L @@ -45,11 +40,11 @@ abstract class BaseAirPodsTest : BaseTest() { .replace("<", "") require(trimmed.length % 2 == 0) { "Not a HEX string" } val bytes = trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray() - mockData(bytes) - block.invoke(factory.create(scanResult) as T) + val result = mockData(bytes) + block.invoke(factory.create(result) as T) } - fun mockData(hex: String) { + fun mockData(hex: String): BleScanResult { val trimmed = hex .replace(" ", "") .replace(">", "") @@ -59,7 +54,9 @@ abstract class BaseAirPodsTest : BaseTest() { return mockData(bytes) } - fun mockData(data: ByteArray) { - every { scanRecord.getManufacturerSpecificData(ContinuityProtocol.APPLE_COMPANY_IDENTIFIER) } returns data - } + fun mockData(data: ByteArray): BleScanResult = baseBleScanResult.copy( + manufacturerSpecificData = mutableMapOf().apply { + this[ContinuityProtocol.APPLE_COMPANY_IDENTIFIER] = data + } + ) } \ No newline at end of file diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/SingleApplePodsTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/SingleApplePodsTest.kt index ef27d319..e60bc0e2 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/apple/SingleApplePodsTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/apple/SingleApplePodsTest.kt @@ -26,11 +26,9 @@ class SingleApplePodsTest : BaseAirPodsTest() { create("07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E") { batteryHeadsetPercent shouldBe 0.5f - isCaseCharging shouldBe false isHeadsetBeingCharged shouldBe false isHeadphonesBeingWorn shouldBe true - batteryCasePercent shouldBe 0.0f } } } \ No newline at end of file diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMaxTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMaxTest.kt index 289a1fa4..a49cfe8f 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMaxTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMaxTest.kt @@ -22,11 +22,9 @@ class AirPodsMaxTest : BaseAirPodsTest() { batteryHeadsetPercent shouldBe 0.4f - isCaseCharging shouldBe false isHeadsetBeingCharged shouldBe false isHeadphonesBeingWorn shouldBe true - batteryCasePercent shouldBe 0.0f } } @@ -45,11 +43,9 @@ class AirPodsMaxTest : BaseAirPodsTest() { batteryHeadsetPercent shouldBe 0.5f - isCaseCharging shouldBe false isHeadsetBeingCharged shouldBe false isHeadphonesBeingWorn shouldBe true - batteryCasePercent shouldBe 0.0f } } } \ No newline at end of file diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsProTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsProTest.kt index 5680702a..be599755 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsProTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsProTest.kt @@ -171,7 +171,7 @@ class AirPodsProTest : BaseAirPodsTest() { isCaseCharging shouldBe false isRightPodCharging shouldBe false isLeftPodCharging shouldBe false - batteryCasePercent shouldBe null + batteryCasePercent shouldBe 0.6f deviceColor shouldBe DualApplePods.DeviceColor.WHITE }