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