Compare commits

..
Author SHA1 Message Date
darken 51f9c5c7d8 Release: 2.6.1-rc0 2023-01-20 12:55:28 +01:00
Matthias Urhahn 15141c8528 More granular pod features (#76)
* Make pod features more granular
Don't assume all AirPods/Beats return a correct connection state.

* Refactoring

* Fix refactoring regression

* AirPods Gen1 don't support state detection
2023-01-18 22:15:38 +01:00
Matthias Urhahn 0cdac10fd4 Update README.md 2023-01-14 16:45:11 +01:00
27 changed files with 155 additions and 147 deletions
+1
View File
@@ -34,6 +34,7 @@ Currently supported models:
* Beats Studio 3 * Beats Studio 3
* Beats X * Beats X
* Beats Flex * Beats Flex
* Beats Fit Pro
* Fake AirPods 1. Generation 🎭 * Fake AirPods 1. Generation 🎭
* Fake AirPods 2. Generation 🎭 * Fake AirPods 2. Generation 🎭
* Fake AirPods 3. Generation 🎭 * Fake AirPods 3. Generation 🎭
+1 -1
View File
@@ -1 +1 @@
2.6.0-rc0 20600000 2.6.1-rc0 20601000
@@ -79,13 +79,13 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
.mapNotNull { it.batteryCasePercent } .mapNotNull { it.batteryCasePercent }
.lastOrNull() .lastOrNull()
fun KnownDevice.getLatestCaseLidState(basic: DualAirPods): DualAirPods.LidState? { fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? {
val definitive = setOf(DualAirPods.LidState.OPEN, DualAirPods.LidState.CLOSED) val definitive = setOf(DualApplePods.LidState.OPEN, DualApplePods.LidState.CLOSED)
if (definitive.contains(basic.caseLidState)) return null if (definitive.contains(basic.caseLidState)) return null
return history return history
.filterIsInstance<AirPodsPro>() // TODO why is this AirPodsPro specific here? .filterIsInstance<AirPodsPro>() // TODO why is this AirPodsPro specific here?
.lastOrNull { it.caseLidState != DualAirPods.LidState.NOT_IN_CASE } .lastOrNull { it.caseLidState != DualApplePods.LidState.NOT_IN_CASE }
?.caseLidState ?.caseLidState
} }
@@ -1,8 +1,5 @@
package eu.darken.capod.pods.core.apple package eu.darken.capod.pods.core.apple
import android.content.Context
import androidx.annotation.StringRes
import eu.darken.capod.common.R
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.isBitSet import eu.darken.capod.common.isBitSet
import eu.darken.capod.common.lowerNibble import eu.darken.capod.common.lowerNibble
@@ -10,8 +7,8 @@ import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.* import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.DualPodDevice.Pod import eu.darken.capod.pods.core.DualPodDevice.Pod
interface DualAirPods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase, interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase,
HasStateDetection, HasDualMicrophone, HasAppleColor { HasDualMicrophone, HasAppleColor {
val primaryPod: Pod val primaryPod: Pod
get() = when (rawStatus.isBitSet(5)) { get() = when (rawStatus.isBitSet(5)) {
@@ -142,21 +139,4 @@ interface DualAirPods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEar
UNKNOWN(0xFF..0xFF); UNKNOWN(0xFF..0xFF);
} }
override val state: ConnectionState
get() = ConnectionState.values().firstOrNull { rawSuffix == it.raw } ?: ConnectionState.UNKNOWN
enum class ConnectionState(val raw: UByte?, @StringRes val labelRes: Int) : HasStateDetection.State {
DISCONNECTED(0x00, R.string.pods_connection_state_disconnected_label),
IDLE(0x04, R.string.pods_connection_state_idle_label),
MUSIC(0x05, R.string.pods_connection_state_music_label),
CALL(0x06, R.string.pods_connection_state_call_label),
RINGING(0x07, R.string.pods_connection_state_ringing_label),
HANGING_UP(0x09, R.string.pods_connection_state_hanging_up_label),
UNKNOWN(null, R.string.pods_connection_state_unknown_label);
override fun getLabel(context: Context): String = context.getString(labelRes)
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
}
} }
@@ -4,9 +4,9 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualAirPods>(tag) { abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualApplePods>(tag) {
fun DualAirPods.getCaseMatchMarkings() = SplitPodsMarkings( fun DualApplePods.getCaseMatchMarkings() = SplitPodsMarkings(
leftPodBattery = batteryLeftPodPercent, leftPodBattery = batteryLeftPodPercent,
rightPodBattery = batteryRightPodPercent, rightPodBattery = batteryRightPodPercent,
microPhoneLeft = isLeftPodMicrophone, microPhoneLeft = isLeftPodMicrophone,
@@ -31,17 +31,17 @@ abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<
val model: PodDevice.Model, val model: PodDevice.Model,
) )
private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualAirPods): Collection<KnownDevice> { private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualApplePods): Collection<KnownDevice> {
val target = device.getCaseMatchMarkings() val target = device.getCaseMatchMarkings()
return filter { known -> return filter { known ->
known.history known.history
.filterIsInstance<DualAirPods>() .filterIsInstance<DualApplePods>()
.any { it.getCaseMatchMarkings() == target } .any { it.getCaseMatchMarkings() == target }
} }
} }
override fun searchHistory(current: DualAirPods): KnownDevice? { override fun searchHistory(current: DualApplePods): KnownDevice? {
val basicResult = super.searchHistory(current) val basicResult = super.searchHistory(current)
val caseIgnored = knownDevices.values.findSplitPodsMatch(current) val caseIgnored = knownDevices.values.findSplitPodsMatch(current)
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePodsFactory import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
@@ -20,15 +20,15 @@ data class AirPodsGen1 constructor(
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualAirPods.LidState? = null private val cachedCaseState: DualApplePods.LidState? = null
) : DualAirPods { ) : DualApplePods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1 override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
override val batteryCasePercent: Float? override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: DualAirPods.LidState override val caseLidState: DualApplePods.LidState
get() = cachedCaseState ?: super.caseLidState get() = cachedCaseState ?: super.caseLidState
override val rssi: Int override val rssi: Int
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePodsFactory import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
@@ -20,19 +20,19 @@ data class AirPodsGen2 constructor(
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualAirPods.LidState? = null private val cachedCaseState: DualApplePods.LidState? = null
) : DualAirPods { ) : DualApplePods, HasStateDetectionAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2 override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
override val batteryCasePercent: Float? override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: DualAirPods.LidState override val caseLidState: DualApplePods.LidState
get() = cachedCaseState ?: super.caseLidState get() = cachedCaseState ?: super.caseLidState
override val rssi: Int override val rssi: Int
get() = rssiAverage ?: super.rssi get() = rssiAverage ?: super<DualApplePods>.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) { class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePodsFactory import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
@@ -20,18 +20,19 @@ data class AirPodsGen3 constructor(
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualAirPods.LidState? = null private val cachedCaseState: DualApplePods.LidState? = null
) : DualAirPods { ) : DualApplePods, HasStateDetectionAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3 override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
override val batteryCasePercent: Float? override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: DualAirPods.LidState override val caseLidState: DualApplePods.LidState
get() = cachedCaseState ?: super.caseLidState get() = cachedCaseState ?: super.caseLidState
override val rssi: Int override val rssi: Int
get() = rssiAverage ?: super.rssi get() = rssiAverage ?: super<DualApplePods>.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) { class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
@@ -21,12 +21,12 @@ data class AirPodsMax(
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
) : SingleApplePods, HasEarDetection { ) : SingleApplePods, HasEarDetection, HasStateDetectionAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
override val rssi: Int override val rssi: Int
get() = rssiAverage ?: super.rssi get() = rssiAverage ?: super<SingleApplePods>.rssi
val isHeadphonesBeingWorn: Boolean val isHeadphonesBeingWorn: Boolean
get() = rawStatus.isBitSet(1) get() = rawStatus.isBitSet(1)
@@ -6,8 +6,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualAirPods.LidState import eu.darken.capod.pods.core.apple.DualApplePods.LidState
import eu.darken.capod.pods.core.apple.DualApplePodsFactory import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
@@ -24,7 +24,7 @@ data class AirPodsPro(
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: LidState? = null private val cachedCaseState: LidState? = null
) : DualAirPods { ) : DualApplePods, HasStateDetectionAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
@@ -51,7 +51,7 @@ data class AirPodsPro(
get() = cachedCaseState ?: super.caseLidState get() = cachedCaseState ?: super.caseLidState
override val rssi: Int override val rssi: Int
get() = rssiAverage ?: super.rssi get() = rssiAverage ?: super<DualApplePods>.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) { class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
@@ -6,8 +6,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualAirPods.LidState import eu.darken.capod.pods.core.apple.DualApplePods.LidState
import eu.darken.capod.pods.core.apple.DualApplePodsFactory import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
@@ -24,7 +24,7 @@ data class AirPodsPro2(
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: LidState? = null private val cachedCaseState: LidState? = null
) : DualAirPods { ) : DualApplePods, HasStateDetectionAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO2 override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO2
@@ -51,7 +51,7 @@ data class AirPodsPro2(
get() = cachedCaseState ?: super.caseLidState get() = cachedCaseState ?: super.caseLidState
override val rssi: Int override val rssi: Int
get() = rssiAverage ?: super.rssi get() = rssiAverage ?: super<HasStateDetectionAirPods>.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) { class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
@@ -0,0 +1,27 @@
package eu.darken.capod.pods.core.apple.airpods
import android.content.Context
import androidx.annotation.StringRes
import eu.darken.capod.common.R
import eu.darken.capod.pods.core.HasStateDetection
import eu.darken.capod.pods.core.apple.ApplePods
interface HasStateDetectionAirPods : HasStateDetection, ApplePods {
override val state: ConnectionState
get() = ConnectionState.values().firstOrNull { rawSuffix == it.raw } ?: ConnectionState.UNKNOWN
enum class ConnectionState(val raw: UByte?, @StringRes val labelRes: Int) : HasStateDetection.State {
DISCONNECTED(0x00, R.string.pods_connection_state_disconnected_label),
IDLE(0x04, R.string.pods_connection_state_idle_label),
MUSIC(0x05, R.string.pods_connection_state_music_label),
CALL(0x06, R.string.pods_connection_state_call_label),
RINGING(0x07, R.string.pods_connection_state_ringing_label),
HANGING_UP(0x09, R.string.pods_connection_state_hanging_up_label),
UNKNOWN(null, R.string.pods_connection_state_unknown_label);
override fun getLabel(context: Context): String = context.getString(labelRes)
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
}
}
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePodsFactory import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
@@ -20,15 +20,15 @@ data class BeatsFitPro(
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualAirPods.LidState? = null private val cachedCaseState: DualApplePods.LidState? = null
) : DualAirPods { ) : DualApplePods {
override val model: PodDevice.Model = PodDevice.Model.BEATS_FIT_PRO override val model: PodDevice.Model = PodDevice.Model.BEATS_FIT_PRO
override val batteryCasePercent: Float? override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: DualAirPods.LidState override val caseLidState: DualApplePods.LidState
get() = cachedCaseState ?: super.caseLidState get() = cachedCaseState ?: super.caseLidState
override val rssi: Int override val rssi: Int
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePodsFactory import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
@@ -20,15 +20,15 @@ data class PowerBeatsPro(
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualAirPods.LidState? = null private val cachedCaseState: DualApplePods.LidState? = null
) : DualAirPods { ) : DualApplePods {
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
override val batteryCasePercent: Float? override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: DualAirPods.LidState override val caseLidState: DualApplePods.LidState
get() = cachedCaseState ?: super.caseLidState get() = cachedCaseState ?: super.caseLidState
override val rssi: Int override val rssi: Int
@@ -11,7 +11,7 @@ import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.* import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
@@ -30,7 +30,7 @@ data class FakeAirPodsPro2 constructor(
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualAirPods.LidState? = null, private val cachedCaseState: DualApplePods.LidState? = null,
) : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase { ) : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase {
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO2 override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO2
@@ -10,7 +10,7 @@ import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.monitor.core.PodMonitor import eu.darken.capod.monitor.core.PodMonitor
import eu.darken.capod.pods.core.HasEarDetection import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.HasEarDetectionDual import eu.darken.capod.pods.core.HasEarDetectionDual
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.reaction.core.ReactionSettings import eu.darken.capod.reaction.core.ReactionSettings
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import javax.inject.Inject import javax.inject.Inject
@@ -69,7 +69,7 @@ class AutoConnect @Inject constructor(
val conditionFulfilled = when (condition) { val conditionFulfilled = when (condition) {
AutoConnectCondition.WHEN_SEEN -> true AutoConnectCondition.WHEN_SEEN -> true
AutoConnectCondition.CASE_OPEN -> when (mainDevice) { AutoConnectCondition.CASE_OPEN -> when (mainDevice) {
is DualAirPods -> mainDevice.caseLidState == DualAirPods.LidState.OPEN is DualApplePods -> mainDevice.caseLidState == DualApplePods.LidState.OPEN
else -> true else -> true
} }
AutoConnectCondition.IN_EAR -> when (mainDevice) { AutoConnectCondition.IN_EAR -> when (mainDevice) {
@@ -7,7 +7,7 @@ import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.common.flow.withPrevious import eu.darken.capod.common.flow.withPrevious
import eu.darken.capod.monitor.core.PodMonitor import eu.darken.capod.monitor.core.PodMonitor
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.reaction.core.ReactionSettings import eu.darken.capod.reaction.core.ReactionSettings
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import java.time.Duration import java.time.Duration
@@ -34,7 +34,7 @@ class PopUpReaction @Inject constructor(
.withPrevious() .withPrevious()
.setupCommonEventHandlers(TAG) { "monitor" } .setupCommonEventHandlers(TAG) { "monitor" }
.mapNotNull { (previous, current) -> .mapNotNull { (previous, current) ->
if (previous !is DualAirPods? || current !is DualAirPods) { if (previous !is DualApplePods? || current !is DualApplePods) {
return@mapNotNull null return@mapNotNull null
} }
log(TAG, VERBOSE) { log(TAG, VERBOSE) {
@@ -57,8 +57,8 @@ class PopUpReaction @Inject constructor(
tryPopWindow(current) tryPopWindow(current)
} }
private suspend fun tryPopWindow(current: DualAirPods): Event? = when { private suspend fun tryPopWindow(current: DualApplePods): Event? = when {
current.caseLidState == DualAirPods.LidState.OPEN -> { current.caseLidState == DualApplePods.LidState.OPEN -> {
log(TAG, INFO) { "Show popup" } log(TAG, INFO) { "Show popup" }
val now = Instant.now() val now = Instant.now()
@@ -74,9 +74,9 @@ class PopUpReaction @Inject constructor(
null null
} }
} }
current.caseLidState != DualAirPods.LidState.OPEN -> { current.caseLidState != DualApplePods.LidState.OPEN -> {
when (current.caseLidState) { when (current.caseLidState) {
DualAirPods.LidState.CLOSED -> { DualApplePods.LidState.CLOSED -> {
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." } log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
coolDowns.remove(current.identifier) coolDowns.remove(current.identifier)
} }
@@ -14,14 +14,14 @@ class AirPodsFactoryTest : BaseAirPodsTest() {
@Test @Test
fun `create AirPodsGen1`() = runTest { fun `create AirPodsGen1`() = runTest {
create<DualAirPods>("07 19 01 02 20 55 AA 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") { create<DualApplePods>("07 19 01 02 20 55 AA 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") {
this shouldBe instanceOf<AirPodsGen1>() this shouldBe instanceOf<AirPodsGen1>()
} }
} }
@Test @Test
fun `create AirPodsPro`() = runTest { fun `create AirPodsPro`() = runTest {
create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") { create<DualApplePods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
this shouldBe instanceOf<AirPodsPro>() this shouldBe instanceOf<AirPodsPro>()
} }
} }
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple package eu.darken.capod.pods.core.apple
import eu.darken.capod.pods.core.apple.airpods.HasStateDetectionAirPods
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -8,7 +9,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test bit mapping`() = runTest { fun `test bit mapping`() = runTest {
create<DualAirPods>("07 19 01 0E 20 54 AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualApplePods>("07 19 01 0E 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() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0e20.toUShort() rawDeviceModel shouldBe 0x0e20.toUShort()
@@ -24,13 +25,13 @@ class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test AirPodDevice - active microphone`() = runTest { fun `test AirPodDevice - active microphone`() = runTest {
create<DualAirPods>("07 19 01 0E 20 >2B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualApplePods>("07 19 01 0E 20 >2B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00101011 // 00101011
// --^----- // --^-----
isLeftPodMicrophone shouldBe true isLeftPodMicrophone shouldBe true
isRightPodMicrophone shouldBe false isRightPodMicrophone shouldBe false
} }
create<DualAirPods>("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualApplePods>("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00001011 // 00001011
// --^----- // --^-----
isLeftPodMicrophone shouldBe false isLeftPodMicrophone shouldBe false
@@ -41,24 +42,24 @@ class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test AirPodDevice - left pod ear status`() = runTest { fun `test AirPodDevice - left pod ear status`() = runTest {
// Left Pod primary // Left Pod primary
create<DualAirPods>("07 19 01 0E 20 >22< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualApplePods>("07 19 01 0E 20 >22< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00100010 // 00100010
// 765432¹0 // 765432¹0
isLeftPodInEar shouldBe true isLeftPodInEar shouldBe true
} }
create<DualAirPods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualApplePods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00100000 // 00100000
// 765432¹0 // 765432¹0
isLeftPodInEar shouldBe false isLeftPodInEar shouldBe false
} }
// Right Pod is primary // Right Pod is primary
create<DualAirPods>("07 19 01 0E 20 >09< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualApplePods>("07 19 01 0E 20 >09< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00001001 // 00001001
// 7654³210 // 7654³210
isLeftPodInEar shouldBe true isLeftPodInEar shouldBe true
} }
create<DualAirPods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualApplePods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00000001 // 00000001
// 7654³210 // 7654³210
isLeftPodInEar shouldBe false isLeftPodInEar shouldBe false
@@ -68,24 +69,24 @@ class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test AirPodDevice - right pod ear status`() = runTest { fun `test AirPodDevice - right pod ear status`() = runTest {
// Left Pod primary // Left Pod primary
create<DualAirPods>("07 19 01 0E 20 >29< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualApplePods>("07 19 01 0E 20 >29< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00101001 // 00101001
// 7654³210 // 7654³210
isRightPodInEar shouldBe true isRightPodInEar shouldBe true
} }
create<DualAirPods>("07 19 01 0E 20 >21< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualApplePods>("07 19 01 0E 20 >21< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00100001 // 00100001
// 7654³210 // 7654³210
isRightPodInEar shouldBe false isRightPodInEar shouldBe false
} }
// Right Pod is primary // Right Pod is primary
create<DualAirPods>("07 19 01 0E 20 >03< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualApplePods>("07 19 01 0E 20 >03< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00000011 // 00000011
// 765432¹0 // 765432¹0
isRightPodInEar shouldBe true isRightPodInEar shouldBe true
} }
create<DualAirPods>("07 19 01 0E 20 >01< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualApplePods>("07 19 01 0E 20 >01< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00000001 // 00000001
// 765432¹0 // 765432¹0
isRightPodInEar shouldBe false isRightPodInEar shouldBe false
@@ -95,13 +96,13 @@ class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test AirPodDevice - battery status`() = runTest { fun `test AirPodDevice - battery status`() = runTest {
// Right Pod is primary // Right Pod is primary
create<DualAirPods>("07 19 01 0E 20 0B >98< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") { create<DualApplePods>("07 19 01 0E 20 0B >98< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
// 88 10001000 // 88 10001000
batteryLeftPodPercent shouldBe 0.9f batteryLeftPodPercent shouldBe 0.9f
batteryRightPodPercent shouldBe 0.8f batteryRightPodPercent shouldBe 0.8f
} }
// Left Pod primary // Left Pod primary
create<DualAirPods>("07 19 01 0E 20 2B >89< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") { create<DualApplePods>("07 19 01 0E 20 2B >89< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
// F8 11111000 // F8 11111000
batteryLeftPodPercent shouldBe 0.9f batteryLeftPodPercent shouldBe 0.9f
batteryRightPodPercent shouldBe 0.8f batteryRightPodPercent shouldBe 0.8f
@@ -114,13 +115,13 @@ class DualApplePodsTest : BaseAirPodsTest() {
* Right pod is charging * Right pod is charging
*/ */
// This is the left // This is the left
create<DualAirPods>("07 19 01 0E 20 51 89 >94< 52 00 00 F4 89 82 6D 3E 27 7F 26 62 57 D0 E2 A6 49 E9 35") { create<DualApplePods>("07 19 01 0E 20 51 89 >94< 52 00 00 F4 89 82 6D 3E 27 7F 26 62 57 D0 E2 A6 49 E9 35") {
// 1001 0100 // 1001 0100
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
isRightPodCharging shouldBe true isRightPodCharging shouldBe true
} }
// This is the right // This is the right
create<DualAirPods>("07 19 01 0E 20 31 98 >A4< 01 00 00 31 B9 A0 C4 80 CD D1 CF B9 3A 9A 6D 48 31 08 EB") { create<DualApplePods>("07 19 01 0E 20 31 98 >A4< 01 00 00 31 B9 A0 C4 80 CD D1 CF B9 3A 9A 6D 48 31 08 EB") {
// 1010 0100 // 1010 0100
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
isRightPodCharging shouldBe true isRightPodCharging shouldBe true
@@ -130,26 +131,26 @@ class DualApplePodsTest : BaseAirPodsTest() {
* Left pod is charging * Left pod is charging
*/ */
// This is the left // This is the left
create<DualAirPods>("07 19 01 0E 20 71 98 >94< 52 00 05 A5 37 31 B2 BD 42 68 0C 64 FD 00 99 4A E5 3E F4") { create<DualApplePods>("07 19 01 0E 20 71 98 >94< 52 00 05 A5 37 31 B2 BD 42 68 0C 64 FD 00 99 4A E5 3E F4") {
// 1001 0100 // 1001 0100
isLeftPodCharging shouldBe true isLeftPodCharging shouldBe true
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
} }
// This is the right // This is the right
create<DualAirPods>("07 19 01 0E 20 11 89 >A4< 04 00 04 BA 79 1B C0 65 69 C6 9F 19 6E 37 7D 6D 86 8D D9") { create<DualApplePods>("07 19 01 0E 20 11 89 >A4< 04 00 04 BA 79 1B C0 65 69 C6 9F 19 6E 37 7D 6D 86 8D D9") {
// 1010 0100 // 1010 0100
isLeftPodCharging shouldBe true isLeftPodCharging shouldBe true
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
} }
// Both charging // Both charging
create<DualAirPods>("07 19 01 0E 20 55 88 >B4< 59 00 05 4B FC DF 68 28 A5 45 52 65 9C FE 51 86 3A B5 DB") { create<DualApplePods>("07 19 01 0E 20 55 88 >B4< 59 00 05 4B FC DF 68 28 A5 45 52 65 9C FE 51 86 3A B5 DB") {
// 1011 0100 // 1011 0100
isLeftPodCharging shouldBe true isLeftPodCharging shouldBe true
isRightPodCharging shouldBe true isRightPodCharging shouldBe true
} }
// Both not charging // Both not charging
create<DualAirPods>("07 19 01 0E 20 00 F8 >8F< 03 00 05 4C 0F A0 C4 05 24 DD EB AF 92 99 FD 54 B1 06 48") { create<DualApplePods>("07 19 01 0E 20 00 F8 >8F< 03 00 05 4C 0F A0 C4 05 24 DD EB AF 92 99 FD 54 B1 06 48") {
// 1000 1111 // 1000 1111
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
@@ -158,11 +159,11 @@ class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test AirPodDevice - case charging`() = runTest { fun `test AirPodDevice - case charging`() = runTest {
create<DualAirPods>("07 19 01 0E 20 75 99 >B4< 31 00 05 77 C8 BA 0C 4E 1F BE AD 70 C5 40 71 D2 E9 17 A2") { create<DualApplePods>("07 19 01 0E 20 75 99 >B4< 31 00 05 77 C8 BA 0C 4E 1F BE AD 70 C5 40 71 D2 E9 17 A2") {
// 0011 0011 // 0011 0011
isCaseCharging shouldBe false isCaseCharging shouldBe false
} }
create<DualAirPods>("07 19 01 0E 20 75 A9 >F4< 51 00 05 A0 37 92 35 49 79 CC DC 27 94 8E FB 72 12 94 52") { create<DualApplePods>("07 19 01 0E 20 75 A9 >F4< 51 00 05 A0 37 92 35 49 79 CC DC 27 94 8E FB 72 12 94 52") {
// 0101 0011 // 0101 0011
isCaseCharging shouldBe true isCaseCharging shouldBe true
} }
@@ -171,75 +172,75 @@ class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test AirPodDevice - case lid test`() = runTest { fun `test AirPodDevice - case lid test`() = runTest {
// Lid open // Lid open
create<DualAirPods>("07 19 01 0E 20 55 AA B4 >31< 00 00 A1 D0 BD 82 D3 52 86 CA FC 11 62 DC 42 C6 92 8E") { create<DualApplePods>("07 19 01 0E 20 55 AA B4 >31< 00 00 A1 D0 BD 82 D3 52 86 CA FC 11 62 DC 42 C6 92 8E") {
// 31 0011 0001 // 31 0011 0001
caseLidState shouldBe DualAirPods.LidState.OPEN caseLidState shouldBe DualApplePods.LidState.OPEN
} }
// Lid open, left pod in case // Lid open, left pod in case
create<DualAirPods>("07 19 01 0E 20 51 9A 93 >31< 00 00 95 D0 A5 D7 E3 F4 F1 38 38 99 61 3B 57 95 37 B7") { create<DualApplePods>("07 19 01 0E 20 51 9A 93 >31< 00 00 95 D0 A5 D7 E3 F4 F1 38 38 99 61 3B 57 95 37 B7") {
// 31 0011 0001 // 31 0011 0001
caseLidState shouldBe DualAirPods.LidState.OPEN caseLidState shouldBe DualApplePods.LidState.OPEN
} }
// Lid open, left pod in case // Lid open, left pod in case
create<DualAirPods>("07 19 01 0E 20 71 A9 92 31 00 00 DB 48 7F 32 8C CE 80 6F D9 27 98 D6 76 45 9D 62") { create<DualApplePods>("07 19 01 0E 20 71 A9 92 31 00 00 DB 48 7F 32 8C CE 80 6F D9 27 98 D6 76 45 9D 62") {
// 31 0011 0001 // 31 0011 0001
caseLidState shouldBe DualAirPods.LidState.OPEN caseLidState shouldBe DualApplePods.LidState.OPEN
} }
// Lid just closed // Lid just closed
create<DualAirPods>("07 19 01 0E 20 55 AA B4 >39< 00 00 08 A6 DB 99 E0 5E 14 85 E5 C2 0B 68 D7 FF C3 A1") { create<DualApplePods>("07 19 01 0E 20 55 AA B4 >39< 00 00 08 A6 DB 99 E0 5E 14 85 E5 C2 0B 68 D7 FF C3 A1") {
// 39 0011 1001 // 39 0011 1001
caseLidState shouldBe DualAirPods.LidState.CLOSED caseLidState shouldBe DualApplePods.LidState.CLOSED
} }
// Lid closed // Lid closed
create<DualAirPods>("07 19 01 0E 20 55 AA B4 38 00 00 F3 F7 08 3B 98 09 C0 DD E4 BD BD 84 55 56 8B 81") { create<DualApplePods>("07 19 01 0E 20 55 AA B4 38 00 00 F3 F7 08 3B 98 09 C0 DD E4 BD BD 84 55 56 8B 81") {
// 38 0011 1000 // 38 0011 1000
caseLidState shouldBe DualAirPods.LidState.CLOSED caseLidState shouldBe DualApplePods.LidState.CLOSED
} }
// Lid closed, right pod in case // Lid closed, right pod in case
create<DualAirPods>("07 19 01 0E 20 51 9A 93 >38< 00 00 3A D8 85 76 B0 91 48 31 DA FF 6C 4A 2B C2 67 F4") { create<DualApplePods>("07 19 01 0E 20 51 9A 93 >38< 00 00 3A D8 85 76 B0 91 48 31 DA FF 6C 4A 2B C2 67 F4") {
// 38 0011 1000 // 38 0011 1000
caseLidState shouldBe DualAirPods.LidState.CLOSED caseLidState shouldBe DualApplePods.LidState.CLOSED
} }
// Lid closed, left pod in case // Lid closed, left pod in case
create<DualAirPods>("07 19 01 0E 20 71 A9 92 38 00 00 44 91 C4 8B 85 98 DD 55 4E 6A CA BC B5 CA 8D 37") { create<DualApplePods>("07 19 01 0E 20 71 A9 92 38 00 00 44 91 C4 8B 85 98 DD 55 4E 6A CA BC B5 CA 8D 37") {
// 38 0011 1000 // 38 0011 1000
caseLidState shouldBe DualAirPods.LidState.CLOSED caseLidState shouldBe DualApplePods.LidState.CLOSED
} }
} }
@Test @Test
fun `test AirPodDevice - connection state`() = runTest { fun `test AirPodDevice - connection state`() = runTest {
// Disconnected // Disconnected
create<DualAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >00< 62 D4 BB F1 A7 F8 64 98 D2 C8 BD 7B 3A EF 2E 15") { create<HasStateDetectionAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >00< 62 D4 BB F1 A7 F8 64 98 D2 C8 BD 7B 3A EF 2E 15") {
// 31 0011 0001 // 31 0011 0001
state shouldBe DualAirPods.ConnectionState.DISCONNECTED state shouldBe HasStateDetectionAirPods.ConnectionState.DISCONNECTED
} }
// Connected idle // Connected idle
create<DualAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >04< 1D 69 69 9C C2 51 F3 1F BF 6E 45 DA 90 4A A3 E3") { create<HasStateDetectionAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >04< 1D 69 69 9C C2 51 F3 1F BF 6E 45 DA 90 4A A3 E3") {
// 39 0011 1001 // 39 0011 1001
state shouldBe DualAirPods.ConnectionState.IDLE state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
} }
// Connected and playing music // Connected and playing music
create<DualAirPods>("07 19 01 0E 20 2B A9 8F 01 00 >05< 14 F7 CB 49 9F D3 B3 22 77 D2 22 F1 74 8C AC A6") { create<HasStateDetectionAirPods>("07 19 01 0E 20 2B A9 8F 01 00 >05< 14 F7 CB 49 9F D3 B3 22 77 D2 22 F1 74 8C AC A6") {
// 38 0011 1000 // 38 0011 1000
state shouldBe DualAirPods.ConnectionState.MUSIC state shouldBe HasStateDetectionAirPods.ConnectionState.MUSIC
} }
// Connected and call active // Connected and call active
create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >06< 0F 4B 43 25 E0 4A 73 63 14 22 C2 3C 89 13 BD 97") { create<HasStateDetectionAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >06< 0F 4B 43 25 E0 4A 73 63 14 22 C2 3C 89 13 BD 97") {
// 38 0011 1000 // 38 0011 1000
state shouldBe DualAirPods.ConnectionState.CALL state shouldBe HasStateDetectionAirPods.ConnectionState.CALL
} }
// Connected and call active // Connected and call active
create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >07< E7 DF 76 44 85 B5 30 F4 95 14 02 DC A1 A4 8A 09") { create<HasStateDetectionAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >07< E7 DF 76 44 85 B5 30 F4 95 14 02 DC A1 A4 8A 09") {
// 38 0011 1000 // 38 0011 1000
state shouldBe DualAirPods.ConnectionState.RINGING state shouldBe HasStateDetectionAirPods.ConnectionState.RINGING
} }
// Switching? // Switching?
create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") { create<HasStateDetectionAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
// 38 0011 1000 // 38 0011 1000
state shouldBe DualAirPods.ConnectionState.HANGING_UP state shouldBe HasStateDetectionAirPods.ConnectionState.HANGING_UP
} }
} }
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.HasAppleColor import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@@ -35,9 +35,7 @@ class AirPodsGen1Test : BaseAirPodsTest() {
isRightPodInEar shouldBe false isRightPodInEar shouldBe false
batteryCasePercent shouldBe 0.6f batteryCasePercent shouldBe 0.6f
caseLidState shouldBe DualAirPods.LidState.OPEN caseLidState shouldBe DualApplePods.LidState.OPEN
state shouldBe DualAirPods.ConnectionState.DISCONNECTED
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.HasAppleColor import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@@ -34,9 +34,9 @@ class AirPodsGen2Test : BaseAirPodsTest() {
isRightPodInEar shouldBe true isRightPodInEar shouldBe true
batteryCasePercent shouldBe null batteryCasePercent shouldBe null
caseLidState shouldBe DualAirPods.LidState.NOT_IN_CASE caseLidState shouldBe DualApplePods.LidState.NOT_IN_CASE
state shouldBe DualAirPods.ConnectionState.MUSIC state shouldBe HasStateDetectionAirPods.ConnectionState.MUSIC
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.HasAppleColor import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@@ -34,9 +34,9 @@ class AirPodsGen3Test : BaseAirPodsTest() {
isRightPodInEar shouldBe false isRightPodInEar shouldBe false
batteryCasePercent shouldBe 0.9f batteryCasePercent shouldBe 0.9f
caseLidState shouldBe DualAirPods.LidState.OPEN caseLidState shouldBe DualApplePods.LidState.OPEN
state shouldBe DualAirPods.ConnectionState.IDLE state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
@@ -58,9 +58,9 @@ class AirPodsGen3Test : BaseAirPodsTest() {
isRightPodInEar shouldBe true isRightPodInEar shouldBe true
batteryCasePercent shouldBe null batteryCasePercent shouldBe null
caseLidState shouldBe DualAirPods.LidState.NOT_IN_CASE caseLidState shouldBe DualApplePods.LidState.NOT_IN_CASE
state shouldBe DualAirPods.ConnectionState.UNKNOWN state shouldBe HasStateDetectionAirPods.ConnectionState.UNKNOWN
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
} }
@@ -8,7 +8,7 @@ import eu.darken.capod.R
import eu.darken.capod.common.lists.binding import eu.darken.capod.common.lists.binding
import eu.darken.capod.databinding.OverviewPodsDualItemBinding import eu.darken.capod.databinding.OverviewPodsDualItemBinding
import eu.darken.capod.pods.core.* import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import java.time.Instant import java.time.Instant
class DualPodsCardVH(parent: ViewGroup) : class DualPodsCardVH(parent: ViewGroup) :
@@ -32,7 +32,7 @@ class DualPodsCardVH(parent: ViewGroup) :
if (item.isMainPod) setTypeface(null, Typeface.BOLD) if (item.isMainPod) setTypeface(null, Typeface.BOLD)
else setTypeface(null, Typeface.NORMAL) else setTypeface(null, Typeface.NORMAL)
if (device is DualAirPods && item.showDebug) { if (device is DualApplePods && item.showDebug) {
append(" [${device.primaryPod.name}]") append(" [${device.primaryPod.name}]")
} }
} }
@@ -8,8 +8,8 @@ import eu.darken.capod.R
import eu.darken.capod.common.lists.binding import eu.darken.capod.common.lists.binding
import eu.darken.capod.databinding.OverviewPodsDualItemBinding import eu.darken.capod.databinding.OverviewPodsDualItemBinding
import eu.darken.capod.pods.core.* import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualAirPods.LidState import eu.darken.capod.pods.core.apple.DualApplePods.LidState
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
@@ -34,7 +34,7 @@ class DualPodsCardVH(parent: ViewGroup) :
if (item.isMainPod) setTypeface(null, Typeface.BOLD) if (item.isMainPod) setTypeface(null, Typeface.BOLD)
else setTypeface(null, Typeface.NORMAL) else setTypeface(null, Typeface.NORMAL)
if (device is DualAirPods && item.showDebug) { if (device is DualApplePods && item.showDebug) {
append(" [${device.primaryPod.name}]") append(" [${device.primaryPod.name}]")
} }
} }
@@ -129,7 +129,7 @@ class DualPodsCardVH(parent: ViewGroup) :
// Case lid state // Case lid state
device.apply { device.apply {
if (this is DualAirPods) { if (this is DualApplePods) {
podCaseLidLabel.text = when (caseLidState) { podCaseLidLabel.text = when (caseLidState) {
LidState.OPEN -> context.getString(R.string.pods_case_status_open_label) LidState.OPEN -> context.getString(R.string.pods_case_status_open_label)
LidState.CLOSED -> context.getString(R.string.pods_case_status_closed_label) LidState.CLOSED -> context.getString(R.string.pods_case_status_closed_label)
@@ -6,7 +6,7 @@ import android.widget.RemoteViews
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.R import eu.darken.capod.R
import eu.darken.capod.pods.core.* import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.SingleApplePods import eu.darken.capod.pods.core.apple.SingleApplePods
import javax.inject.Inject import javax.inject.Inject
@@ -16,12 +16,12 @@ class MonitorNotificationViewFactory @Inject constructor(
) { ) {
fun createContentView(device: PodDevice): RemoteViews = when (device) { fun createContentView(device: PodDevice): RemoteViews = when (device) {
is DualAirPods -> createDualApplePods(device) is DualApplePods -> createDualApplePods(device)
is SingleApplePods -> createSingleApplePods(device) is SingleApplePods -> createSingleApplePods(device)
else -> createUnknownDevice(device) else -> createUnknownDevice(device)
} }
private fun createDualApplePods(device: DualAirPods): RemoteViews = RemoteViews( private fun createDualApplePods(device: DualApplePods): RemoteViews = RemoteViews(
context.packageName, context.packageName,
R.layout.monitor_notification_dual_pods_small R.layout.monitor_notification_dual_pods_small
).apply { ).apply {
@@ -12,7 +12,7 @@ import eu.darken.capod.common.debug.autoreport.DebugSettings
import eu.darken.capod.databinding.PopupNotificationDualPodsBinding import eu.darken.capod.databinding.PopupNotificationDualPodsBinding
import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding
import eu.darken.capod.pods.core.* import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.SingleApplePods import eu.darken.capod.pods.core.apple.SingleApplePods
import javax.inject.Inject import javax.inject.Inject
@@ -26,13 +26,13 @@ class PopUpPodViewFactory @Inject constructor(
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
fun createContentView(parent: ViewGroup, device: PodDevice): View = when (device) { fun createContentView(parent: ViewGroup, device: PodDevice): View = when (device) {
is DualAirPods -> createDualApplePods(parent, device) is DualApplePods -> createDualApplePods(parent, device)
// Unused, has no case to trigger reaction? // Unused, has no case to trigger reaction?
is SingleApplePods -> createSingleApplePods(parent, device) is SingleApplePods -> createSingleApplePods(parent, device)
else -> throw IllegalArgumentException("Unexpected device: $device") else -> throw IllegalArgumentException("Unexpected device: $device")
} }
private fun createDualApplePods(parent: ViewGroup, device: DualAirPods): View = private fun createDualApplePods(parent: ViewGroup, device: DualApplePods): View =
PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply { PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply {
device.apply { device.apply {
podIcon.setImageResource(iconRes) podIcon.setImageResource(iconRes)
+1 -1
View File
@@ -1,6 +1,6 @@
### Updated by release.sh ### ### Updated by release.sh ###
project.versioning.major=2 project.versioning.major=2
project.versioning.minor=6 project.versioning.minor=6
project.versioning.patch=0 project.versioning.patch=1
project.versioning.build=0 project.versioning.build=0
############################# #############################