mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Add option to show fake devices
This commit is contained in:
@@ -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
|
||||
|
||||
+2
@@ -32,6 +32,8 @@ class UnknownPodDeviceCardVH(parent: ViewGroup) :
|
||||
RelativeDateTimeFormatter.RelativeUnit.SECONDS
|
||||
)
|
||||
|
||||
reception.text = device.getSignalQuality(context)
|
||||
|
||||
rawdata.text = device.rawDataHex
|
||||
}
|
||||
|
||||
|
||||
+40
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
+32
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user