mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Support for more pods + tests
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
package eu.darken.capod.common
|
||||
|
||||
import android.os.SystemClock
|
||||
|
||||
object SystemClockWrap {
|
||||
|
||||
val elapsedRealtimeNanos: Long
|
||||
get() = SystemClock.elapsedRealtimeNanos()
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
|
||||
@@ -18,7 +18,7 @@ import eu.darken.capod.main.ui.overview.cards.pods.UnknownPodDeviceCardVH
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.monitor.core.worker.MonitorControl
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.airpods.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.util.*
|
||||
|
||||
+3
-3
@@ -6,9 +6,9 @@ import eu.darken.capod.R
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.databinding.OverviewPodsAppleDualItemBinding
|
||||
import eu.darken.capod.pods.core.DualPods
|
||||
import eu.darken.capod.pods.core.airpods.DualApplePods
|
||||
import eu.darken.capod.pods.core.airpods.DualApplePods.DeviceColor
|
||||
import eu.darken.capod.pods.core.airpods.DualApplePods.LidState
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.DeviceColor
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||
import eu.darken.capod.pods.core.getBatteryCase
|
||||
import eu.darken.capod.pods.core.getBatteryLeftPod
|
||||
import eu.darken.capod.pods.core.getBatteryRightPod
|
||||
|
||||
@@ -13,7 +13,7 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.monitor.core.worker.MonitorControl
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -20,7 +20,7 @@ import eu.darken.capod.monitor.core.MonitorComponent
|
||||
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
||||
import eu.darken.capod.monitor.core.MonitorSettings
|
||||
import eu.darken.capod.monitor.ui.MonitorNotifications
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@@ -20,7 +20,7 @@ import eu.darken.capod.common.hasApiLevel
|
||||
import eu.darken.capod.common.notifications.PendingIntentCompat
|
||||
import eu.darken.capod.main.ui.MainActivity
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.airpods.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.getBatteryCase
|
||||
import eu.darken.capod.pods.core.getBatteryLeftPod
|
||||
import eu.darken.capod.pods.core.getBatteryRightPod
|
||||
@@ -53,7 +53,7 @@ class MonitorNotifications @Inject constructor(
|
||||
.setChannelId(NOTIFICATION_CHANNEL_ID)
|
||||
.setContentIntent(openPi)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setSmallIcon(R.drawable.ic_baseline_earbuds_24)
|
||||
.setSmallIcon(R.drawable.ic_device_generic_earbuds)
|
||||
.setOngoing(true)
|
||||
.setContentTitle(context.getString(R.string.app_name))
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class MonitorNotifications @Inject constructor(
|
||||
fun getBuilder(device: PodDevice?): NotificationCompat.Builder {
|
||||
if (device == null) {
|
||||
builder.setContentTitle(context.getString(R.string.device_status_loading_message))
|
||||
builder.setSmallIcon(R.drawable.ic_baseline_earbuds_24)
|
||||
builder.setSmallIcon(R.drawable.ic_device_generic_earbuds)
|
||||
return builder
|
||||
}
|
||||
|
||||
|
||||
@@ -28,5 +28,5 @@ interface PodDevice {
|
||||
|
||||
@get:DrawableRes
|
||||
val iconRes: Int
|
||||
get() = R.drawable.ic_baseline_earbuds_24
|
||||
get() = R.drawable.ic_device_generic_earbuds
|
||||
}
|
||||
@@ -6,18 +6,18 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.airpods.AirPodsFactory
|
||||
import eu.darken.capod.pods.core.apple.AppleFactory
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class PodFactory @Inject constructor(
|
||||
private val airPodsFactory: AirPodsFactory
|
||||
private val appleFactory: AppleFactory
|
||||
) {
|
||||
|
||||
suspend fun createPod(scanResult: ScanResult): PodDevice? {
|
||||
log(TAG, VERBOSE) { "Trying to create Pod for $scanResult" }
|
||||
|
||||
val pod = airPodsFactory.create(scanResult)
|
||||
val pod = appleFactory.create(scanResult)
|
||||
if (pod != null) {
|
||||
log(TAG) { "Pod created: $pod" }
|
||||
return pod
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package eu.darken.capod.pods.core.airpods
|
||||
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing
|
||||
|
||||
interface ApplePods : PodDevice {
|
||||
|
||||
val proximityMessage: ProximityPairing.Message
|
||||
|
||||
companion object {
|
||||
|
||||
val TAG = logTag("Pod", "AppleDevice")
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package eu.darken.capod.pods.core.airpods
|
||||
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.DualPods
|
||||
import eu.darken.capod.pods.core.DualPods.Pod
|
||||
|
||||
interface SingleApplePods : ApplePods, DualPods {
|
||||
|
||||
val tag: String
|
||||
|
||||
// We start counting at the airpods prefix byte
|
||||
val rawPrefix: UByte
|
||||
get() = proximityMessage.data[0]
|
||||
|
||||
val rawDeviceModel: UShort
|
||||
get() = (((proximityMessage.data[1].toInt() and 255) shl 8) or (proximityMessage.data[2].toInt() and 255)).toUShort()
|
||||
|
||||
val rawStatus: UByte
|
||||
get() = proximityMessage.data[3]
|
||||
|
||||
val rawPodsBattery: UByte
|
||||
get() = proximityMessage.data[4]
|
||||
|
||||
val rawCaseBattery: UByte
|
||||
get() = proximityMessage.data[5]
|
||||
|
||||
val rawCaseLidState: UByte
|
||||
get() = proximityMessage.data[6]
|
||||
|
||||
val rawDeviceColor: UByte
|
||||
get() = proximityMessage.data[7]
|
||||
|
||||
val rawSuffix: UByte
|
||||
get() = proximityMessage.data[8]
|
||||
|
||||
override val microPhonePod: Pod
|
||||
get() = when (rawStatus.isBitSet(5)) {
|
||||
true -> Pod.LEFT
|
||||
false -> Pod.RIGHT
|
||||
}
|
||||
|
||||
override val batteryLeftPodPercent: Float?
|
||||
get() {
|
||||
val value = when (microPhonePod) {
|
||||
Pod.LEFT -> rawPodsBattery.lowerNibble.toInt()
|
||||
Pod.RIGHT -> rawPodsBattery.upperNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log(tag) { "Left pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
(value / 10f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val batteryRightPodPercent: Float?
|
||||
get() {
|
||||
val value = when (microPhonePod) {
|
||||
Pod.LEFT -> rawPodsBattery.upperNibble.toInt()
|
||||
Pod.RIGHT -> rawPodsBattery.lowerNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log(tag) { "Right pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = when (val value = rawCaseBattery.lowerNibble.toInt()) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log(tag) { "Case: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
|
||||
override val isLeftPodInEar: Boolean
|
||||
get() = when (microPhonePod) {
|
||||
Pod.LEFT -> rawStatus.isBitSet(1)
|
||||
Pod.RIGHT -> rawStatus.isBitSet(3)
|
||||
}
|
||||
|
||||
override val isRightPodInEar: Boolean
|
||||
get() = when (microPhonePod) {
|
||||
Pod.LEFT -> rawStatus.isBitSet(3)
|
||||
Pod.RIGHT -> rawStatus.isBitSet(1)
|
||||
}
|
||||
|
||||
val status: Status
|
||||
get() = Status.values().firstOrNull { it.raw == rawStatus } ?: Status.UNKNOWN
|
||||
|
||||
enum class Status(val raw: UByte?) {
|
||||
BOTH_AIRPODS_IN_CASE(0x55),
|
||||
LEFT_IN_EAR(0x33),
|
||||
AIRPLANE(0x02),
|
||||
UNKNOWN(null);
|
||||
|
||||
constructor(raw: Int) : this(raw.toUByte())
|
||||
}
|
||||
|
||||
// 1010101 0x55 85 Both In Case
|
||||
// 0101011 0x2b 43 Both In Ear
|
||||
// 0101001 0x29 41 Right in Ear
|
||||
// 0100011 0x23 35 Left In Ear
|
||||
// 0100001 0x21 33 Neither in Ear or In Case
|
||||
// 1110001 0x71 113 Left in Case, Right on Desk
|
||||
// 0010001 0x11 17 Left in Case, Right on Desk
|
||||
// 0010011 0x13 19 Left in Case, Right in Ear
|
||||
// 1110011 0x73 115 Left in Case, Right in Ear
|
||||
|
||||
|
||||
override val isCaseCharging: Boolean
|
||||
get() = rawCaseBattery.upperNibble.isBitSet(2)
|
||||
|
||||
override val isLeftPodCharging: Boolean
|
||||
get() = when (microPhonePod) {
|
||||
Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(0)
|
||||
Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodCharging: Boolean
|
||||
get() = when (microPhonePod) {
|
||||
Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(1)
|
||||
Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(0)
|
||||
}
|
||||
|
||||
val caseLidState: LidState
|
||||
get() = LidState.values().firstOrNull { it.raw == rawCaseLidState } ?: LidState.UNKNOWN
|
||||
|
||||
enum class LidState(val raw: UByte?) {
|
||||
OPEN(0x31),
|
||||
CLOSED(0x38),
|
||||
NOT_IN_CASE(0x01),
|
||||
UNKNOWN(null);
|
||||
|
||||
constructor(raw: Int) : this(raw.toUByte())
|
||||
}
|
||||
|
||||
val deviceColor: DeviceColor
|
||||
get() = DeviceColor.values().firstOrNull { it.raw == rawDeviceColor } ?: DeviceColor.UNKNOWN
|
||||
|
||||
enum class DeviceColor(val raw: UByte?) {
|
||||
WHITE(0x00),
|
||||
BLACK(0x01),
|
||||
RED(0x02),
|
||||
BLUE(0x03),
|
||||
PINK(0x04),
|
||||
GRAY(0x05),
|
||||
SILVER(0x06),
|
||||
GOLD(0x07),
|
||||
ROSE_GOLD(0x08),
|
||||
SPACE_GRAY(0x09),
|
||||
DARK_BLUE(0x0a),
|
||||
LIGHT_BLUE(0x0b),
|
||||
YELLOW(0x0c),
|
||||
UNKNOWN(null);
|
||||
|
||||
constructor(raw: Int) : this(raw.toUByte())
|
||||
}
|
||||
|
||||
val connectionState: ConnectionState
|
||||
get() = ConnectionState.values().firstOrNull { rawSuffix == it.raw } ?: ConnectionState.UNKNOWN
|
||||
|
||||
enum class ConnectionState(val raw: UByte?) {
|
||||
DISCONNECTED(0x00),
|
||||
IDLE(0x04),
|
||||
MUSIC(0x05),
|
||||
CALL(0x06),
|
||||
RINGING(0x07),
|
||||
HANGING_UP(0x09),
|
||||
UNKNOWN(null);
|
||||
|
||||
constructor(raw: Int) : this(raw.toUByte())
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package eu.darken.capod.pods.core.airpods.models
|
||||
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.content.Context
|
||||
import eu.darken.capod.pods.core.airpods.ApplePods
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
data class AirPodsMax constructor(
|
||||
override val identifier: UUID = UUID.randomUUID(),
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: ScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
) : ApplePods {
|
||||
|
||||
override fun getLabel(context: Context): String {
|
||||
return "AirPods Max"
|
||||
}
|
||||
|
||||
}
|
||||
+47
-34
@@ -1,19 +1,17 @@
|
||||
package eu.darken.capod.pods.core.airpods
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.os.SystemClock
|
||||
import eu.darken.capod.common.SystemClockWrap
|
||||
import eu.darken.capod.common.debug.Bugs
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.airpods.models.AirPodsGen1
|
||||
import eu.darken.capod.pods.core.airpods.models.AirPodsGen2
|
||||
import eu.darken.capod.pods.core.airpods.models.AirPodsPro
|
||||
import eu.darken.capod.pods.core.airpods.models.UnknownAppleDevice
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing
|
||||
import eu.darken.capod.pods.core.apple.airpods.*
|
||||
import eu.darken.capod.pods.core.apple.beats.*
|
||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.time.Duration
|
||||
@@ -22,7 +20,7 @@ import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AirPodsFactory @Inject constructor(
|
||||
class AppleFactory @Inject constructor(
|
||||
private val continuityProtocolDecoder: ContinuityProtocol.Decoder,
|
||||
private val proximityPairingDecoder: ProximityPairing.Decoder
|
||||
) {
|
||||
@@ -41,7 +39,7 @@ class AirPodsFactory @Inject constructor(
|
||||
get() = Duration.ofNanos(scanResult.timestampNanos)
|
||||
|
||||
fun isOlderThan(age: Duration): Boolean {
|
||||
val now = Duration.ofNanos(SystemClock.elapsedRealtimeNanos())
|
||||
val now = Duration.ofNanos(SystemClockWrap.elapsedRealtimeNanos)
|
||||
return now - timestampNanos > age
|
||||
}
|
||||
}
|
||||
@@ -139,42 +137,57 @@ class AirPodsFactory @Inject constructor(
|
||||
val dmDirty = pm.data[1]
|
||||
|
||||
return when {
|
||||
dm == 0x0220.toUShort() || dmDirty == 2.toUByte() -> AirPodsGen1(
|
||||
dm == 0x0220.toUShort() -> AirPodsGen1(
|
||||
identifier = identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm
|
||||
)
|
||||
dm == 0x0F20.toUShort() || dmDirty == 15.toUByte() -> AirPodsGen2(
|
||||
dm == 0x0F20.toUShort() -> AirPodsGen2(
|
||||
identifier = identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm
|
||||
)
|
||||
dm == 0x0e20.toUShort() || dmDirty == 14.toUByte() -> AirPodsPro(
|
||||
dm == 0x0e20.toUShort() -> AirPodsPro(
|
||||
identifier = identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm
|
||||
)
|
||||
// dmDirty == 10.toUByte() -> {
|
||||
// TODO("Airpods Max")
|
||||
// }
|
||||
// dmDirty == 11.toUByte() -> {
|
||||
// TODO("PowerBeatsPro")
|
||||
// }
|
||||
// dm == 0x0520.toUShort() || dmDirty == 5.toUByte() -> {
|
||||
// TODO("BeatsX")
|
||||
// }
|
||||
// dmDirty == 0.toUByte() -> {
|
||||
// TODO("BeatsFlex")
|
||||
// }
|
||||
// dm == 0x0620.toUShort() || dmDirty == 6.toUByte() -> {
|
||||
// TODO("Beats Solo 3")
|
||||
// }
|
||||
// dmDirty == 9.toUByte() -> {
|
||||
// TODO("Beats Studio 3")
|
||||
// }
|
||||
// dm == 0x0320.toUShort() || dmDirty == 3.toUByte() -> {
|
||||
// TODO("Power Beats 3")
|
||||
// }
|
||||
dmDirty == 10.toUByte() -> AirPodsMax(
|
||||
identifier = identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm
|
||||
)
|
||||
dm == 0x0320.toUShort() -> PowerBeats3(
|
||||
identifier = identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm
|
||||
)
|
||||
dm == 0x0620.toUShort() -> BeatsSolo3(
|
||||
identifier = identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm
|
||||
)
|
||||
dmDirty == 9.toUByte() -> BeatsStudio3(
|
||||
identifier = identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm
|
||||
)
|
||||
dmDirty == 11.toUByte() -> PowerBeatsPro(
|
||||
identifier = identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm
|
||||
)
|
||||
dm == 0x0520.toUShort() || dmDirty == 5.toUByte() -> BeatsX(
|
||||
identifier = identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm
|
||||
)
|
||||
dm == 0x1020.toUShort() -> BeatsFlex(
|
||||
identifier = identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm
|
||||
)
|
||||
|
||||
else -> {
|
||||
log(TAG, WARN) { "Unknown proximity message type" }
|
||||
Bugs.report(IllegalArgumentException("Unknown ProximityMessage: $pm"))
|
||||
@@ -0,0 +1,42 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
|
||||
interface ApplePods : PodDevice {
|
||||
|
||||
val tag: String
|
||||
|
||||
val proximityMessage: ProximityPairing.Message
|
||||
|
||||
// We start counting at the airpods prefix byte
|
||||
val rawPrefix: UByte
|
||||
get() = proximityMessage.data[0]
|
||||
|
||||
val rawDeviceModel: UShort
|
||||
get() = (((proximityMessage.data[1].toInt() and 255) shl 8) or (proximityMessage.data[2].toInt() and 255)).toUShort()
|
||||
|
||||
val rawStatus: UByte
|
||||
get() = proximityMessage.data[3]
|
||||
|
||||
val rawPodsBattery: UByte
|
||||
get() = proximityMessage.data[4]
|
||||
|
||||
val rawCaseBattery: UByte
|
||||
get() = proximityMessage.data[5]
|
||||
|
||||
val rawCaseLidState: UByte
|
||||
get() = proximityMessage.data[6]
|
||||
|
||||
val rawDeviceColor: UByte
|
||||
get() = proximityMessage.data[7]
|
||||
|
||||
val rawSuffix: UByte
|
||||
get() = proximityMessage.data[8]
|
||||
|
||||
companion object {
|
||||
|
||||
val TAG = logTag("Pod", "AppleDevice")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
|
||||
interface BasicSingleApplePods : ApplePods {
|
||||
|
||||
val batteryHeadsetPercent: Float?
|
||||
get() = when (val value = rawPodsBattery.lowerNibble.toInt()) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log(tag) { "Left pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
(value / 10f)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-28
@@ -1,4 +1,4 @@
|
||||
package eu.darken.capod.pods.core.airpods
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
@@ -12,33 +12,6 @@ import eu.darken.capod.pods.core.DualPods.Pod
|
||||
|
||||
interface DualApplePods : ApplePods, DualPods {
|
||||
|
||||
val tag: String
|
||||
|
||||
// We start counting at the airpods prefix byte
|
||||
val rawPrefix: UByte
|
||||
get() = proximityMessage.data[0]
|
||||
|
||||
val rawDeviceModel: UShort
|
||||
get() = (((proximityMessage.data[1].toInt() and 255) shl 8) or (proximityMessage.data[2].toInt() and 255)).toUShort()
|
||||
|
||||
val rawStatus: UByte
|
||||
get() = proximityMessage.data[3]
|
||||
|
||||
val rawPodsBattery: UByte
|
||||
get() = proximityMessage.data[4]
|
||||
|
||||
val rawCaseBattery: UByte
|
||||
get() = proximityMessage.data[5]
|
||||
|
||||
val rawCaseLidState: UByte
|
||||
get() = proximityMessage.data[6]
|
||||
|
||||
val rawDeviceColor: UByte
|
||||
get() = proximityMessage.data[7]
|
||||
|
||||
val rawSuffix: UByte
|
||||
get() = proximityMessage.data[8]
|
||||
|
||||
override val microPhonePod: Pod
|
||||
get() = when (rawStatus.isBitSet(5)) {
|
||||
true -> Pod.LEFT
|
||||
@@ -0,0 +1,29 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
|
||||
interface SingleApplePods : BasicSingleApplePods {
|
||||
|
||||
val batteryCasePercent: Float?
|
||||
get() = when (val value = rawCaseBattery.lowerNibble.toInt()) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log(tag) { "Case: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
|
||||
val isHeadsetBeingWorn: Boolean
|
||||
get() = rawStatus.isBitSet(1)
|
||||
|
||||
val isCaseCharging: Boolean
|
||||
get() = rawCaseBattery.upperNibble.isBitSet(2)
|
||||
|
||||
val isHeadsetBeingCharged: Boolean
|
||||
get() = rawCaseBattery.upperNibble.isBitSet(0)
|
||||
}
|
||||
+6
-4
@@ -1,9 +1,10 @@
|
||||
package eu.darken.capod.pods.core.airpods.models
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.content.Context
|
||||
import eu.darken.capod.pods.core.airpods.ApplePods
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
@@ -15,7 +16,8 @@ data class UnknownAppleDevice constructor(
|
||||
) : ApplePods {
|
||||
|
||||
override fun getLabel(context: Context): String {
|
||||
return "Unknown device"
|
||||
return context.getString(R.string.device_unknown_label)
|
||||
}
|
||||
|
||||
override val tag: String = logTag("Pod", "Apple", "Unknown")
|
||||
}
|
||||
+5
-7
@@ -1,11 +1,11 @@
|
||||
package eu.darken.capod.pods.core.airpods.models
|
||||
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.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.airpods.DualApplePods
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
@@ -16,12 +16,10 @@ data class AirPodsGen1 constructor(
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
) : DualApplePods {
|
||||
|
||||
override fun getLabel(context: Context): String {
|
||||
return "AirPods (Gen 1)"
|
||||
}
|
||||
override fun getLabel(context: Context): String = "AirPods (Gen 1)"
|
||||
|
||||
override val iconRes: Int
|
||||
get() = R.drawable.ic_device_airpods_gen1
|
||||
|
||||
override val tag: String = logTag("Pod", "AirPodsGen1")
|
||||
override val tag: String = logTag("Pod", "Apple", "AirPods", "Gen1")
|
||||
}
|
||||
+5
-7
@@ -1,11 +1,11 @@
|
||||
package eu.darken.capod.pods.core.airpods.models
|
||||
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.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.airpods.DualApplePods
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
@@ -16,12 +16,10 @@ data class AirPodsGen2 constructor(
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
) : DualApplePods {
|
||||
|
||||
override fun getLabel(context: Context): String {
|
||||
return "AirPods (Gen 2)"
|
||||
}
|
||||
override fun getLabel(context: Context): String = "AirPods (Gen 2)"
|
||||
|
||||
override val iconRes: Int
|
||||
get() = R.drawable.ic_device_airpods_gen2
|
||||
|
||||
override val tag: String = logTag("Pod", "AirPodsGen2")
|
||||
override val tag: String = logTag("Pod", "Apple", "AirPods", "Gen2")
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
data class AirPodsMax constructor(
|
||||
override val identifier: UUID = UUID.randomUUID(),
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: ScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
) : SingleApplePods {
|
||||
|
||||
override fun getLabel(context: Context): String = "AirPods Max"
|
||||
|
||||
override val iconRes: Int
|
||||
get() = R.drawable.ic_device_generic_headphones
|
||||
|
||||
override val tag: String = logTag("Pod", "Apple", "AirPods", "Max")
|
||||
}
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
package eu.darken.capod.pods.core.airpods.models
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.content.Context
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.airpods.DualApplePods
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
@@ -19,5 +19,5 @@ data class AirPodsPro constructor(
|
||||
return "AirPods Pro"
|
||||
}
|
||||
|
||||
override val tag: String = logTag("Pod", "AirPodsPro")
|
||||
override val tag: String = logTag("Pod", "Apple", "AirPods", "Pro")
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
data class BeatsFlex constructor(
|
||||
override val identifier: UUID = UUID.randomUUID(),
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: ScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
) : BasicSingleApplePods {
|
||||
|
||||
override fun getLabel(context: Context): String = "Beats Flex"
|
||||
|
||||
override val iconRes: Int
|
||||
get() = R.drawable.ic_device_generic_earbuds
|
||||
|
||||
override val tag: String = logTag("Pod", "Apple", "Beats", "Flex")
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
data class BeatsSolo3 constructor(
|
||||
override val identifier: UUID = UUID.randomUUID(),
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: ScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
) : BasicSingleApplePods {
|
||||
|
||||
override fun getLabel(context: Context): String = "Beats Solo 3"
|
||||
|
||||
override val iconRes: Int
|
||||
get() = R.drawable.ic_device_generic_earbuds
|
||||
|
||||
override val tag: String = logTag("Pod", "Apple", "Beats", "Solo3")
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
data class BeatsStudio3 constructor(
|
||||
override val identifier: UUID = UUID.randomUUID(),
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: ScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
) : BasicSingleApplePods {
|
||||
|
||||
override fun getLabel(context: Context): String = "Beats Studio 3"
|
||||
|
||||
override val iconRes: Int
|
||||
get() = R.drawable.ic_device_generic_earbuds
|
||||
|
||||
override val tag: String = logTag("Pod", "Apple", "Beats", "Studio3")
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
data class BeatsX constructor(
|
||||
override val identifier: UUID = UUID.randomUUID(),
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: ScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
) : BasicSingleApplePods {
|
||||
|
||||
override fun getLabel(context: Context): String = "Beats X"
|
||||
|
||||
override val iconRes: Int
|
||||
get() = R.drawable.ic_device_generic_earbuds
|
||||
|
||||
override val tag: String = logTag("Pod", "Apple", "Beats", "X")
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
data class PowerBeats3 constructor(
|
||||
override val identifier: UUID = UUID.randomUUID(),
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: ScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
) : BasicSingleApplePods {
|
||||
|
||||
override fun getLabel(context: Context): String = "Power Beats 3"
|
||||
|
||||
override val iconRes: Int
|
||||
get() = R.drawable.ic_device_generic_earbuds
|
||||
|
||||
override val tag: String = logTag("Pod", "Apple", "Beats", "Power3")
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
data class PowerBeatsPro constructor(
|
||||
override val identifier: UUID = UUID.randomUUID(),
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: ScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message
|
||||
) : DualApplePods {
|
||||
|
||||
override fun getLabel(context: Context): String = "Power Beats Pro"
|
||||
|
||||
override val iconRes: Int
|
||||
get() = R.drawable.ic_device_generic_earbuds
|
||||
|
||||
override val tag: String = logTag("Pod", "Apple", "Beats", "PowerPro")
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package eu.darken.capod.pods.core.airpods.protocol
|
||||
package eu.darken.capod.pods.core.apple.protocol
|
||||
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.os.ParcelUuid
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package eu.darken.capod.pods.core.airpods.protocol
|
||||
package eu.darken.capod.pods.core.apple.protocol
|
||||
|
||||
import android.bluetooth.le.ScanFilter
|
||||
import dagger.Reusable
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.pods.core.airpods.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<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="M12,3c-4.97,0 -9,4.03 -9,9v7c0,1.1 0.9,2 2,2h4v-8H5v-1c0,-3.87 3.13,-7 7,-7s7,3.13 7,7v1h-4v8h4c1.1,0 2,-0.9 2,-2v-7C21,7.03 16.97,3 12,3z" />
|
||||
</vector>
|
||||
@@ -22,7 +22,7 @@
|
||||
app:layout_constraintBottom_toBottomOf="@id/name"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/name"
|
||||
app:srcCompat="@drawable/ic_baseline_earbuds_24" />
|
||||
app:srcCompat="@drawable/ic_device_generic_earbuds" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/name"
|
||||
|
||||
@@ -81,4 +81,5 @@
|
||||
<string name="settings_support_email_developer_description">Note that I can only respond in german or english.</string>
|
||||
<string name="settings_debug_autoreports_label">Automatic bug reports</string>
|
||||
<string name="settings_debug_autoreports_description">Automatically reports issues, e.g. details on an app crash so I can figure out how to fix it.</string>
|
||||
<string name="device_unknown_label">Unknown device</string>
|
||||
</resources>
|
||||
+4
-59
@@ -1,70 +1,15 @@
|
||||
package eu.darken.capod.pods.core.airpods
|
||||
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.pods.core.DualPods
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.airpods.models.AirPodsGen1
|
||||
import eu.darken.capod.pods.core.airpods.models.AirPodsPro
|
||||
import eu.darken.capod.pods.core.airpods.models.UnknownAppleDevice
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing
|
||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen1
|
||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.types.instanceOf
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelper.BaseTest
|
||||
|
||||
class AirPodsFactoryTest : BaseTest() {
|
||||
|
||||
@MockK lateinit var scanResult: ScanResult
|
||||
@MockK lateinit var scanRecord: ScanRecord
|
||||
@MockK lateinit var device: BluetoothDevice
|
||||
|
||||
val factory = AirPodsFactory(
|
||||
proximityPairingDecoder = ProximityPairing.Decoder(),
|
||||
continuityProtocolDecoder = ContinuityProtocol.Decoder(),
|
||||
)
|
||||
|
||||
@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"
|
||||
}
|
||||
|
||||
private suspend inline fun <reified T : PodDevice?> create(hex: String, block: T.() -> Unit) {
|
||||
val trimmed = hex
|
||||
.replace(" ", "")
|
||||
.replace(">", "")
|
||||
.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)
|
||||
}
|
||||
|
||||
private fun mockData(hex: String) {
|
||||
val trimmed = hex
|
||||
.replace(" ", "")
|
||||
.replace(">", "")
|
||||
.replace("<", "")
|
||||
require(trimmed.length % 2 == 0) { "Not a HEX string" }
|
||||
val bytes = trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
return mockData(bytes)
|
||||
}
|
||||
|
||||
private fun mockData(data: ByteArray) {
|
||||
every { scanRecord.getManufacturerSpecificData(ContinuityProtocol.APPLE_COMPANY_IDENTIFIER) } returns data
|
||||
}
|
||||
class AirPodsFactoryTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - active microphone`() = runBlockingTest {
|
||||
+9
-4
@@ -1,14 +1,16 @@
|
||||
package eu.darken.capod.pods.core.airpods
|
||||
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.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing
|
||||
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
|
||||
|
||||
@@ -18,7 +20,7 @@ abstract class BaseAirPodsTest : BaseTest() {
|
||||
@MockK lateinit var scanRecord: ScanRecord
|
||||
@MockK lateinit var device: BluetoothDevice
|
||||
|
||||
val factory = AirPodsFactory(
|
||||
val factory = AppleFactory(
|
||||
proximityPairingDecoder = ProximityPairing.Decoder(),
|
||||
continuityProtocolDecoder = ContinuityProtocol.Decoder(),
|
||||
)
|
||||
@@ -31,6 +33,9 @@ abstract class BaseAirPodsTest : BaseTest() {
|
||||
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
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : PodDevice?> create(hex: String, block: T.() -> Unit) {
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
package eu.darken.capod.pods.core.airpods.models
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.airpods.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.airpods.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
package eu.darken.capod.pods.core.airpods.models
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.airpods.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.airpods.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -0,0 +1,55 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsMaxTest : BaseAirPodsTest() {
|
||||
|
||||
// Test data from https://github.com/adolfintel/OpenPods/issues/124
|
||||
@Test
|
||||
fun `default AirPods Max`() = runBlockingTest {
|
||||
create<AirPodsMax>("07 19 01 0A 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0A20.toUShort()
|
||||
rawStatus shouldBe 0x62.toUByte()
|
||||
rawPodsBattery shouldBe 0x04.toUByte()
|
||||
rawCaseBattery shouldBe 0x80.toUByte()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
isHeadsetBeingWorn shouldBe true
|
||||
batteryCasePercent shouldBe 0.0f
|
||||
}
|
||||
}
|
||||
|
||||
// Test data from https://github.com/adolfintel/OpenPods/issues/124
|
||||
@Test
|
||||
fun `default AirPods Max flipped values`() = runBlockingTest {
|
||||
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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0A20.toUShort()
|
||||
rawStatus shouldBe 0x02.toUByte()
|
||||
rawPodsBattery shouldBe 0x05.toUByte()
|
||||
rawCaseBattery shouldBe 0x80.toUByte()
|
||||
rawCaseLidState shouldBe 0x04.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x44.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
isHeadsetBeingWorn shouldBe true
|
||||
batteryCasePercent shouldBe 0.0f
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
package eu.darken.capod.pods.core.airpods.models
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.DualPods
|
||||
import eu.darken.capod.pods.core.airpods.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.airpods.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -0,0 +1,26 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsFlexText : BaseAirPodsTest() {
|
||||
|
||||
// Raw data from https://github.com/adolfintel/OpenPods/issues/105
|
||||
@Test
|
||||
fun `default BeatsFlex`() = runBlockingTest {
|
||||
create<BeatsFlex>("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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1020.toUShort()
|
||||
rawStatus shouldBe 0x0A.toUByte()
|
||||
rawPodsBattery shouldBe 0xF4.toUByte()
|
||||
rawCaseBattery shouldBe 0x8F.toUByte()
|
||||
rawCaseLidState shouldBe 0x00.toUByte()
|
||||
rawDeviceColor shouldBe 0x01.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsSolo3Test : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@Test
|
||||
fun `default BeatsSolo3`() = runBlockingTest {
|
||||
create<BeatsSolo3>("07 19 01 06 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0620.toUShort()
|
||||
rawStatus shouldBe 0x62.toUByte()
|
||||
rawPodsBattery shouldBe 0x04.toUByte()
|
||||
rawCaseBattery shouldBe 0x80.toUByte()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsStudio3Test : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@Test
|
||||
fun `default BeatsStudio3`() = runBlockingTest {
|
||||
create<BeatsStudio3>("07 19 01 09 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0920.toUShort()
|
||||
rawStatus shouldBe 0x62.toUByte()
|
||||
rawPodsBattery shouldBe 0x04.toUByte()
|
||||
rawCaseBattery shouldBe 0x80.toUByte()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsXTest : BaseAirPodsTest() {
|
||||
|
||||
// Raw data from https://github.com/adolfintel/OpenPods/issues/105
|
||||
@Test
|
||||
fun `default BeatsX`() = runBlockingTest {
|
||||
create<BeatsX>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0520.toUShort()
|
||||
rawStatus shouldBe 0x00.toUByte()
|
||||
rawPodsBattery shouldBe 0xF5.toUByte()
|
||||
rawCaseBattery shouldBe 0x0F.toUByte()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x01.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PowerBeats3Test : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@Test
|
||||
fun `default PowerBeats3`() = runBlockingTest {
|
||||
create<PowerBeats3>("07 19 01 03 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0320.toUShort()
|
||||
rawStatus shouldBe 0x62.toUByte()
|
||||
rawPodsBattery shouldBe 0x04.toUByte()
|
||||
rawCaseBattery shouldBe 0x80.toUByte()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.DualPods
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PowerBeatsProTest : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@Test
|
||||
fun `test PowerBeatsPro`() = runBlockingTest {
|
||||
create<PowerBeatsPro>("07 19 01 0B 20 54 AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0B20.toUShort()
|
||||
rawStatus shouldBe 0x54.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawCaseBattery shouldBe 0xB5.toUByte()
|
||||
rawCaseLidState shouldBe 0x31.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
microPhonePod shouldBe DualPods.Pod.RIGHT
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
isLeftPodCharging shouldBe true
|
||||
batteryCasePercent shouldBe 0.5f
|
||||
|
||||
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user