diff --git a/app/src/main/java/eu/darken/capod/common/SystemClockWrap.kt b/app/src/main/java/eu/darken/capod/common/SystemClockWrap.kt
new file mode 100644
index 00000000..8c402e78
--- /dev/null
+++ b/app/src/main/java/eu/darken/capod/common/SystemClockWrap.kt
@@ -0,0 +1,9 @@
+package eu.darken.capod.common
+
+import android.os.SystemClock
+
+object SystemClockWrap {
+
+ val elapsedRealtimeNanos: Long
+ get() = SystemClock.elapsedRealtimeNanos()
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt
index aaca78d3..7800b007 100644
--- a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt
+++ b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt
@@ -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
diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt
index 70be9f2c..85b03d90 100644
--- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt
+++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt
@@ -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.*
diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/DualApplePodsCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/DualApplePodsCardVH.kt
index 42e5becb..bf21eaf9 100644
--- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/DualApplePodsCardVH.kt
+++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/DualApplePodsCardVH.kt
@@ -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
diff --git a/app/src/main/java/eu/darken/capod/monitor/core/receiver/BluetoothEventReceiver.kt b/app/src/main/java/eu/darken/capod/monitor/core/receiver/BluetoothEventReceiver.kt
index 2d544bc5..629a41ac 100644
--- a/app/src/main/java/eu/darken/capod/monitor/core/receiver/BluetoothEventReceiver.kt
+++ b/app/src/main/java/eu/darken/capod/monitor/core/receiver/BluetoothEventReceiver.kt
@@ -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
diff --git a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt
index f65ce472..d899e142 100644
--- a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt
+++ b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt
@@ -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
diff --git a/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotifications.kt b/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotifications.kt
index 7a0398de..a344a429 100644
--- a/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotifications.kt
+++ b/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotifications.kt
@@ -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
}
diff --git a/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt b/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt
index fcea4145..66a02da8 100644
--- a/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt
+++ b/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt
@@ -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
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/PodFactory.kt b/app/src/main/java/eu/darken/capod/pods/core/PodFactory.kt
index 1af684c4..45fc62e1 100644
--- a/app/src/main/java/eu/darken/capod/pods/core/PodFactory.kt
+++ b/app/src/main/java/eu/darken/capod/pods/core/PodFactory.kt
@@ -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
diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/ApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/airpods/ApplePods.kt
deleted file mode 100644
index 33df1d5a..00000000
--- a/app/src/main/java/eu/darken/capod/pods/core/airpods/ApplePods.kt
+++ /dev/null
@@ -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")
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/SingleApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/airpods/SingleApplePods.kt
deleted file mode 100644
index 7aacec38..00000000
--- a/app/src/main/java/eu/darken/capod/pods/core/airpods/SingleApplePods.kt
+++ /dev/null
@@ -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())
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsMax.kt b/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsMax.kt
deleted file mode 100644
index af08444f..00000000
--- a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsMax.kt
+++ /dev/null
@@ -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"
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/AirPodsFactory.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt
similarity index 73%
rename from app/src/main/java/eu/darken/capod/pods/core/airpods/AirPodsFactory.kt
rename to app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt
index fd762578..adfda152 100644
--- a/app/src/main/java/eu/darken/capod/pods/core/airpods/AirPodsFactory.kt
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt
@@ -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"))
diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt
new file mode 100644
index 00000000..b762ffc1
--- /dev/null
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt
@@ -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")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/BasicSingleApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/BasicSingleApplePods.kt
new file mode 100644
index 00000000..699aa98b
--- /dev/null
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/BasicSingleApplePods.kt
@@ -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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/DualApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt
similarity index 87%
rename from app/src/main/java/eu/darken/capod/pods/core/airpods/DualApplePods.kt
rename to app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt
index 62b31894..5268f8b5 100644
--- a/app/src/main/java/eu/darken/capod/pods/core/airpods/DualApplePods.kt
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt
@@ -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
diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/SingleApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/SingleApplePods.kt
new file mode 100644
index 00000000..5de84e21
--- /dev/null
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/SingleApplePods.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/UnknownAppleDevice.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt
similarity index 59%
rename from app/src/main/java/eu/darken/capod/pods/core/airpods/models/UnknownAppleDevice.kt
rename to app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt
index f51599e9..10d7c9b9 100644
--- a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/UnknownAppleDevice.kt
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt
@@ -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")
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen1.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt
similarity index 62%
rename from app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen1.kt
rename to app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt
index da55d1af..865260ad 100644
--- a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen1.kt
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt
@@ -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")
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen2.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt
similarity index 62%
rename from app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen2.kt
rename to app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt
index 6ea38d8e..328331c3 100644
--- a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen2.kt
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt
@@ -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")
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt
new file mode 100644
index 00000000..9be0ff5e
--- /dev/null
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt
@@ -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")
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsPro.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt
similarity index 68%
rename from app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsPro.kt
rename to app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt
index 63f10163..21b6c6d6 100644
--- a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsPro.kt
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt
@@ -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")
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt
new file mode 100644
index 00000000..71de3da9
--- /dev/null
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt
@@ -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")
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt
new file mode 100644
index 00000000..298e2c53
--- /dev/null
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt
@@ -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")
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt
new file mode 100644
index 00000000..70ea0f92
--- /dev/null
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt
@@ -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")
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt
new file mode 100644
index 00000000..66fb9f09
--- /dev/null
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt
@@ -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")
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt
new file mode 100644
index 00000000..b1272ecf
--- /dev/null
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt
@@ -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")
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt
new file mode 100644
index 00000000..9e2d7e16
--- /dev/null
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt
@@ -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")
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/protocol/ContinuityProtocol.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/protocol/ContinuityProtocol.kt
similarity index 97%
rename from app/src/main/java/eu/darken/capod/pods/core/airpods/protocol/ContinuityProtocol.kt
rename to app/src/main/java/eu/darken/capod/pods/core/apple/protocol/ContinuityProtocol.kt
index 40167022..c0a3e7a3 100644
--- a/app/src/main/java/eu/darken/capod/pods/core/airpods/protocol/ContinuityProtocol.kt
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/protocol/ContinuityProtocol.kt
@@ -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
diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/protocol/ProximityPairing.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/protocol/ProximityPairing.kt
similarity index 94%
rename from app/src/main/java/eu/darken/capod/pods/core/airpods/protocol/ProximityPairing.kt
rename to app/src/main/java/eu/darken/capod/pods/core/apple/protocol/ProximityPairing.kt
index bd7e5841..8f1df3dd 100644
--- a/app/src/main/java/eu/darken/capod/pods/core/airpods/protocol/ProximityPairing.kt
+++ b/app/src/main/java/eu/darken/capod/pods/core/apple/protocol/ProximityPairing.kt
@@ -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
diff --git a/app/src/main/res/drawable/ic_baseline_earbuds_24.xml b/app/src/main/res/drawable/ic_device_generic_earbuds.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_baseline_earbuds_24.xml
rename to app/src/main/res/drawable/ic_device_generic_earbuds.xml
diff --git a/app/src/main/res/drawable/ic_device_generic_headphones.xml b/app/src/main/res/drawable/ic_device_generic_headphones.xml
new file mode 100644
index 00000000..e7f4419e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_device_generic_headphones.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/layout/overview_pods_apple_dual_item.xml b/app/src/main/res/layout/overview_pods_apple_dual_item.xml
index 6ac0b1a2..2fae122a 100644
--- a/app/src/main/res/layout/overview_pods_apple_dual_item.xml
+++ b/app/src/main/res/layout/overview_pods_apple_dual_item.xml
@@ -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" />
Note that I can only respond in german or english.
Automatic bug reports
Automatically reports issues, e.g. details on an app crash so I can figure out how to fix it.
+ Unknown device
\ No newline at end of file
diff --git a/app/src/test/java/eu/darken/capod/pods/core/airpods/AirPodsFactoryTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/AirPodsFactoryTest.kt
similarity index 79%
rename from app/src/test/java/eu/darken/capod/pods/core/airpods/AirPodsFactoryTest.kt
rename to app/src/test/java/eu/darken/capod/pods/core/apple/AirPodsFactoryTest.kt
index 25dd8e8a..133464b0 100644
--- a/app/src/test/java/eu/darken/capod/pods/core/airpods/AirPodsFactoryTest.kt
+++ b/app/src/test/java/eu/darken/capod/pods/core/apple/AirPodsFactoryTest.kt
@@ -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 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 {
diff --git a/app/src/test/java/eu/darken/capod/pods/core/airpods/BaseAirPodsTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/BaseAirPodsTest.kt
similarity index 83%
rename from app/src/test/java/eu/darken/capod/pods/core/airpods/BaseAirPodsTest.kt
rename to app/src/test/java/eu/darken/capod/pods/core/apple/BaseAirPodsTest.kt
index 0bc8ac0d..d16904af 100644
--- a/app/src/test/java/eu/darken/capod/pods/core/airpods/BaseAirPodsTest.kt
+++ b/app/src/test/java/eu/darken/capod/pods/core/apple/BaseAirPodsTest.kt
@@ -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 create(hex: String, block: T.() -> Unit) {
diff --git a/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen1Test.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1Test.kt
similarity index 89%
rename from app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen1Test.kt
rename to app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1Test.kt
index f505e71b..13ae1aaa 100644
--- a/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen1Test.kt
+++ b/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1Test.kt
@@ -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
diff --git a/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen2Test.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2Test.kt
similarity index 89%
rename from app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen2Test.kt
rename to app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2Test.kt
index 7c32a749..7ba41dce 100644
--- a/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen2Test.kt
+++ b/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2Test.kt
@@ -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
diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMaxTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMaxTest.kt
new file mode 100644
index 00000000..16deef0d
--- /dev/null
+++ b/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMaxTest.kt
@@ -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("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("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
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsProTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsProTest.kt
similarity index 97%
rename from app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsProTest.kt
rename to app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsProTest.kt
index 1a93d449..f26ec535 100644
--- a/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsProTest.kt
+++ b/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsProTest.kt
@@ -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
diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsFlexText.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsFlexText.kt
new file mode 100644
index 00000000..c1199b74
--- /dev/null
+++ b/app/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsFlexText.kt
@@ -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("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
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3Test.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3Test.kt
new file mode 100644
index 00000000..180ab3af
--- /dev/null
+++ b/app/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3Test.kt
@@ -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("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
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3Test.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3Test.kt
new file mode 100644
index 00000000..f6c1354b
--- /dev/null
+++ b/app/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3Test.kt
@@ -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("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
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsXTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsXTest.kt
new file mode 100644
index 00000000..27514e0f
--- /dev/null
+++ b/app/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsXTest.kt
@@ -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("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
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3Test.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3Test.kt
new file mode 100644
index 00000000..9419c0cb
--- /dev/null
+++ b/app/src/test/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3Test.kt
@@ -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("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
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsProTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsProTest.kt
new file mode 100644
index 00000000..340ca068
--- /dev/null
+++ b/app/src/test/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsProTest.kt
@@ -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("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
+ }
+ }
+}
\ No newline at end of file