Add option to show fake devices

This commit is contained in:
darken
2022-01-06 11:59:30 +01:00
parent 5637cff557
commit f9d39a6319
34 changed files with 307 additions and 107 deletions
@@ -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<Int, ByteArray>
) : 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<Int, ByteArray>().apply {
scanResult.scanRecord?.manufacturerSpecificData?.forEach { key, value ->
this[key] = value
}
}
)
}
}
@@ -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<ScanFilter> = ProximityPairing.getBleScanFilter(),
mode: Int = ScanSettings.SCAN_MODE_BALANCED,
): Flow<List<ScanResult>> = callbackFlow {
): Flow<List<BleScanResult>> = 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<ScanResult>) {
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<BleScanResult>()
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")
@@ -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,
)
}
@@ -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")
}
}
@@ -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
@@ -32,6 +32,8 @@ class UnknownPodDeviceCardVH(parent: ViewGroup) :
RelativeDateTimeFormatter.RelativeUnit.SECONDS
)
reception.text = device.getSignalQuality(context)
rawdata.text = device.rawDataHex
}
@@ -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<Preference>("debug.log.record")!!
vm.state.observe2(this) {
logPref.summary = it.currentLogPath?.path
}
logPref.setOnPreferenceClickListener {
vm.toggleRecorder()
true
}
super.onViewCreated(view, savedInstanceState)
}
}
@@ -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")
}
}
@@ -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
@@ -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)
@@ -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<PodDevice.Id, ValueCache>()
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 {
@@ -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
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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<Message> = scanResult.scanRecord
?.getManufacturerSpecificData(APPLE_COMPANY_IDENTIFIER)
fun decode(scanResult: BleScanResult): List<Message> = scanResult
.getManufacturerSpecificData(APPLE_COMPANY_IDENTIFIER)
?.let { data ->
val messages = mutableListOf<Message>()
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M15,4l0,2l3,0l0,12l-3,0l0,2l5,0l0,-16z" />
<path
android:fillColor="@android:color/white"
android:pathData="M4,20l5,0l0,-2l-3,0l0,-12l3,0l0,-2l-5,0z" />
</vector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M20.41,8.41l-4.83,-4.83C15.21,3.21 14.7,3 14.17,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V9.83C21,9.3 20.79,8.79 20.41,8.41zM7,7h7v2H7V7zM17,17H7v-2h10V17zM17,13H7v-2h10V13z" />
</vector>
+5 -5
View File
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_item_settings"
android:title="@string/settings_general_label" />
<item
android:id="@+id/menu_item_debuglog"
android:title="@string/debug_debuglog_record_action" />
android:icon="@drawable/ic_baseline_settings_24"
android:title="@string/settings_general_label"
app:showAsAction="always" />
</menu>
+4
View File
@@ -103,4 +103,8 @@
<string name="pods_single_basic_status_short">Headphones: %1$s</string>
<string name="permission_background_location_label">ACCESS_BACKGROUND_LOCATION</string>
<string name="permission_background_location_description">Allows an app to access location in the background.</string>
<string name="settings_fake_data_label">Fake data</string>
<string name="settings_fake_data_description">Show fake data, i.e. simulate device that don\'t exist.</string>
<string name="settings_debug_label">Debug settings</string>
<string name="settings_debug_description">Additional settings to help troubleshoot issues with the app.</string>
</resources>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:icon="@drawable/ic_baseline_text_snippet_24"
android:key="debug.log.record"
android:title="@string/debug_debuglog_record_action" />
<CheckBoxPreference
android:icon="@drawable/ic_baseline_bug_report_24"
android:key="debug.mode.enabled"
android:summary="@string/settings_debug_mode_description"
android:title="@string/settings_debug_mode_label" />
<CheckBoxPreference
android:icon="@drawable/ic_baseline_data_array_24"
android:key="debug.fakedata.enabled"
android:summary="@string/settings_fake_data_description"
android:title="@string/settings_fake_data_label" />
</PreferenceScreen>
+5 -4
View File
@@ -39,11 +39,12 @@
android:summary="@string/settings_debug_autoreports_description"
android:title="@string/settings_debug_autoreports_label" />
<CheckBoxPreference
<Preference
android:icon="@drawable/ic_baseline_bug_report_24"
android:key="debug.mode.enabled"
android:summary="@string/settings_debug_mode_description"
android:title="@string/settings_debug_mode_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" />
</PreferenceCategory>
@@ -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<Int, ByteArray>().apply {
this[ContinuityProtocol.APPLE_COMPANY_IDENTIFIER] = data
}
)
}
@@ -26,11 +26,9 @@ class SingleApplePodsTest : BaseAirPodsTest() {
create<AirPodsMax>("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
}
}
}
@@ -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
}
}
}
@@ -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
}