Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51f9c5c7d8 | ||
|
|
15141c8528 | ||
|
|
0cdac10fd4 | ||
|
|
aaab548bfd | ||
|
|
46611b5606 | ||
|
|
fdbdb46577 | ||
|
|
e7fd81d0eb | ||
|
|
ce50bae6a0 | ||
|
|
8331cf3866 | ||
|
|
670262e53b | ||
|
|
c584f942cf | ||
|
|
e83b6bb583 | ||
|
|
cc5de668f0 | ||
|
|
653c7b8c4b | ||
|
|
4e38bcd4cc | ||
|
|
fcbeda4888 | ||
|
|
4101fddc5b | ||
|
|
86df7bea22 | ||
|
|
4db8c0c286 | ||
|
|
1d03335903 | ||
|
|
b1b541014f | ||
|
|
b40a7e435f | ||
|
|
8305fb4c5f |
@@ -34,15 +34,21 @@ Currently supported models:
|
||||
* Beats Studio 3
|
||||
* Beats X
|
||||
* Beats Flex
|
||||
* Beats Fit Pro
|
||||
* Fake AirPods 1. Generation 🎭
|
||||
* Fake AirPods 2. Generation 🎭
|
||||
* Fake AirPods 3. Generation 🎭
|
||||
* Fake AirPods Pro1 🎭
|
||||
* Fake AirPods Pro2 🎭
|
||||
|
||||
## Download
|
||||
|
||||
| Source | Status |
|
||||
|-----------------------|--------|
|
||||
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [](https://play.google.com/store/apps/details?id=eu.darken.capod) |
|
||||
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | ? |
|
||||
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [](https://github.com/d4rken-org/capod/releases/latest) |
|
||||
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [](https://apt.izzysoft.de/packages/eu.darken.capod/) |
|
||||
| Source | Status |
|
||||
|------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [](https://play.google.com/store/apps/details?id=eu.darken.capod) |
|
||||
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [](https://play.google.com/apps/testing/eu.darken.capod) | |
|
||||
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [](https://github.com/d4rken-org/capod/releases/latest) |
|
||||
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [](https://apt.izzysoft.de/packages/eu.darken.capod/) |
|
||||
|
||||
|
||||
## Support the project
|
||||
|
||||
@@ -104,17 +104,28 @@ interface PodDevice {
|
||||
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
|
||||
"Power Beats Pro"
|
||||
),
|
||||
@Json(name = "beats.fit.pro") BEATS_FIT_PRO(
|
||||
"Beats Fit Pro"
|
||||
),
|
||||
@Json(name = "fakes.tws.i99999") FAKE_AIRPODS_GEN1(
|
||||
"AirPods (Gen 1)? \uD83C\uDFAD",
|
||||
R.drawable.devic_airpods_gen1_both,
|
||||
),
|
||||
@Json(name = "fakes.generic.airpods.gen2") FAKE_AIRPODS_GEN2(
|
||||
"AirPods (Gen 2)? \uD83C\uDFAD",
|
||||
R.drawable.devic_airpods_gen2_both,
|
||||
),
|
||||
@Json(name = "fakes.generic.airpods.gen3") FAKE_AIRPODS_GEN3(
|
||||
"AirPods (Gen 3)? \uD83C\uDFAD",
|
||||
R.drawable.devic_airpods_gen2_both,
|
||||
),
|
||||
@Json(name = "fakes.varunr.airpodspro") FAKE_AIRPODS_PRO(
|
||||
"AirPods Pro? \uD83C\uDFAD",
|
||||
R.drawable.devic_airpods_pro2_both,
|
||||
),
|
||||
@Json(name = "fakes.generic.airpods.gen3") FAKE_AIRPODS_GEN3(
|
||||
"AirPods (Gen 3)? \uD83C\uDFAD",
|
||||
R.drawable.devic_airpods_gen2_both,
|
||||
@Json(name = "fakes.generic.airpods.pro2") FAKE_AIRPODS_PRO2(
|
||||
"AirPods Pro2? \uD83C\uDFAD",
|
||||
R.drawable.devic_airpods_pro2_both,
|
||||
),
|
||||
@Json(name = "unknown") UNKNOWN(
|
||||
"Unknown"
|
||||
|
||||
@@ -7,9 +7,7 @@ import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import eu.darken.capod.pods.core.apple.airpods.*
|
||||
import eu.darken.capod.pods.core.apple.beats.*
|
||||
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen1
|
||||
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen3
|
||||
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsPro
|
||||
import eu.darken.capod.pods.core.apple.misc.*
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
@@ -29,8 +27,11 @@ abstract class AppleFactoryModule {
|
||||
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun beatsFitPro(factory: BeatsFitPro.Factory): ApplePodsFactory<out ApplePods>
|
||||
|
||||
@Binds @IntoSet abstract fun fakeAirPodsGen1(factory: FakeAirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun fakeAirPodsPro(factory: FakeAirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun fakeAirPodsGen2(factory: FakeAirPodsGen2.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun fakeAirPodsGen3(factory: FakeAirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun fakeAirPodsPro(factory: FakeAirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun fakeAirPodsPro2(factory: FakeAirPodsPro2.Factory): ApplePodsFactory<out ApplePods>
|
||||
}
|
||||
@@ -79,13 +79,13 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
.mapNotNull { it.batteryCasePercent }
|
||||
.lastOrNull()
|
||||
|
||||
fun KnownDevice.getLatestCaseLidState(basic: DualAirPods): DualAirPods.LidState? {
|
||||
val definitive = setOf(DualAirPods.LidState.OPEN, DualAirPods.LidState.CLOSED)
|
||||
fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? {
|
||||
val definitive = setOf(DualApplePods.LidState.OPEN, DualApplePods.LidState.CLOSED)
|
||||
if (definitive.contains(basic.caseLidState)) return null
|
||||
|
||||
return history
|
||||
.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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
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.isBitSet
|
||||
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.DualPodDevice.Pod
|
||||
|
||||
interface DualAirPods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase,
|
||||
HasStateDetection, HasDualMicrophone, HasAppleColor {
|
||||
interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase,
|
||||
HasDualMicrophone, HasAppleColor {
|
||||
|
||||
val primaryPod: Pod
|
||||
get() = when (rawStatus.isBitSet(5)) {
|
||||
@@ -142,21 +139,4 @@ interface DualAirPods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEar
|
||||
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.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,
|
||||
rightPodBattery = batteryRightPodPercent,
|
||||
microPhoneLeft = isLeftPodMicrophone,
|
||||
@@ -31,17 +31,17 @@ abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<
|
||||
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()
|
||||
|
||||
return filter { known ->
|
||||
known.history
|
||||
.filterIsInstance<DualAirPods>()
|
||||
.filterIsInstance<DualApplePods>()
|
||||
.any { it.getCaseMatchMarkings() == target }
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchHistory(current: DualAirPods): KnownDevice? {
|
||||
override fun searchHistory(current: DualApplePods): KnownDevice? {
|
||||
val basicResult = super.searchHistory(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.pods.core.PodDevice
|
||||
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.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
@@ -20,15 +20,15 @@ data class AirPodsGen1 constructor(
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualAirPods.LidState? = null
|
||||
) : DualAirPods {
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
) : DualApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
override val caseLidState: DualAirPods.LidState
|
||||
override val caseLidState: DualApplePods.LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
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.pods.core.PodDevice
|
||||
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.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
@@ -20,19 +20,19 @@ data class AirPodsGen2 constructor(
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualAirPods.LidState? = null
|
||||
) : DualAirPods {
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
) : DualApplePods, HasStateDetectionAirPods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
override val caseLidState: DualAirPods.LidState
|
||||
override val caseLidState: DualApplePods.LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||
|
||||
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.pods.core.PodDevice
|
||||
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.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
@@ -20,18 +20,19 @@ data class AirPodsGen3 constructor(
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualAirPods.LidState? = null
|
||||
) : DualAirPods {
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
) : DualApplePods, HasStateDetectionAirPods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
override val caseLidState: DualAirPods.LidState
|
||||
override val caseLidState: DualApplePods.LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
|
||||
@@ -21,12 +21,12 @@ data class AirPodsMax(
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods, HasEarDetection {
|
||||
) : SingleApplePods, HasEarDetection, HasStateDetectionAirPods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
get() = rssiAverage ?: super<SingleApplePods>.rssi
|
||||
|
||||
val isHeadphonesBeingWorn: Boolean
|
||||
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.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
@@ -24,7 +24,7 @@ data class AirPodsPro(
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: LidState? = null
|
||||
) : DualAirPods {
|
||||
) : DualApplePods, HasStateDetectionAirPods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
||||
|
||||
@@ -51,7 +51,7 @@ data class AirPodsPro(
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.common.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
@@ -22,10 +24,26 @@ data class AirPodsPro2(
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: LidState? = null
|
||||
) : DualAirPods {
|
||||
) : DualApplePods, HasStateDetectionAirPods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO2
|
||||
|
||||
@get:DrawableRes
|
||||
override val iconRes: Int
|
||||
get() = R.drawable.devic_airpods_pro2_both
|
||||
|
||||
@get:DrawableRes
|
||||
override val caseIcon: Int
|
||||
get() = R.drawable.devic_airpods_pro2_case
|
||||
|
||||
@get:DrawableRes
|
||||
override val leftPodIcon: Int
|
||||
get() = R.drawable.devic_airpods_pro2_left
|
||||
|
||||
@get:DrawableRes
|
||||
override val rightPodIcon: Int
|
||||
get() = R.drawable.devic_airpods_pro2_right
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
@@ -33,7 +51,7 @@ data class AirPodsPro2(
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
get() = rssiAverage ?: super<HasStateDetectionAirPods>.rssi
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
data class BeatsFitPro(
|
||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||
override val seenLastAt: Instant = Instant.now(),
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
) : DualApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_FIT_PRO
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
override val caseLidState: DualApplePods.LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = BeatsFitPro(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEVICE_CODE = 0x1220.toUShort()
|
||||
private val TAG = logTag("PodDevice", "Beats", "Fit", "Pro")
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
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.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
@@ -20,15 +20,15 @@ data class PowerBeatsPro(
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualAirPods.LidState? = null
|
||||
) : DualAirPods {
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
) : DualApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
override val caseLidState: DualAirPods.LidState
|
||||
override val caseLidState: DualApplePods.LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
|
||||
@@ -6,10 +6,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
@@ -31,7 +28,7 @@ data class FakeAirPodsGen1 constructor(
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
|
||||
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN1
|
||||
|
||||
@@ -79,22 +76,32 @@ data class FakeAirPodsGen1 constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val isThisPodInThecase: Boolean
|
||||
private val isThisPodInThecase: Boolean
|
||||
get() = rawStatus.isBitSet(6)
|
||||
|
||||
/**
|
||||
* The data flip bit is set if the left pod is primary.
|
||||
* For the pod that is in the case, this is flipped again though.
|
||||
*/
|
||||
override val isLeftPodMicrophone: Boolean
|
||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||
override val isLeftPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(3)
|
||||
false -> rawStatus.isBitSet(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* The data flip bit is UNset if the right pod is primary.
|
||||
* For the pod that is in the case, this is flipped again though.
|
||||
*/
|
||||
override val isRightPodMicrophone: Boolean
|
||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||
override val isRightPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(1)
|
||||
false -> rawStatus.isBitSet(3)
|
||||
}
|
||||
|
||||
override val isLeftPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(0)
|
||||
true -> rawFlags.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(1)
|
||||
true -> rawFlags.isBitSet(0)
|
||||
}
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = when (val value = rawCaseBattery.toInt()) {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Similar data structure but a lot of placeholder values or hardcoded values
|
||||
*/
|
||||
data class FakeAirPodsGen2 constructor(
|
||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||
override val seenLastAt: Instant = Instant.now(),
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN2
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||
|
||||
/**
|
||||
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
|
||||
* If the right pod is the primary, the values are flipped.
|
||||
*/
|
||||
val areValuesFlipped: Boolean
|
||||
get() = !rawStatus.isBitSet(5)
|
||||
|
||||
override val batteryLeftPodPercent: Float?
|
||||
get() {
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.upperNibble.toInt()
|
||||
false -> rawPodsBattery.lowerNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Left pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
(value / 10f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val batteryRightPodPercent: Float?
|
||||
get() {
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.lowerNibble.toInt()
|
||||
false -> rawPodsBattery.upperNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Right pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val isThisPodInThecase: Boolean
|
||||
get() = rawStatus.isBitSet(6)
|
||||
|
||||
override val isLeftPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(3)
|
||||
false -> rawStatus.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(1)
|
||||
false -> rawStatus.isBitSet(3)
|
||||
}
|
||||
|
||||
override val isLeftPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(0)
|
||||
true -> rawFlags.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(1)
|
||||
true -> rawFlags.isBitSet(0)
|
||||
}
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = when (val value = rawCaseBattery.toInt()) {
|
||||
15 -> cachedBatteryPercentage
|
||||
else -> if (value > 10) {
|
||||
log { "Case: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
|
||||
override val isCaseCharging: Boolean
|
||||
get() = rawFlags.isBitSet(2)
|
||||
|
||||
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsGen2>(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||
getModelInfo().full == DEVICE_CODE && length == 19
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = FakeAirPodsGen2(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEVICE_CODE = 0x0F20.toUShort()
|
||||
private val TAG = logTag("PodDevice", "Apple", "Fake", "AirPods", "Gen2")
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ data class FakeAirPodsGen3 constructor(
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase, HasChargeDetectionDual {
|
||||
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN3
|
||||
|
||||
@@ -88,22 +88,20 @@ data class FakeAirPodsGen3 constructor(
|
||||
true -> rawFlags.isBitSet(0)
|
||||
}
|
||||
|
||||
val isThisPodInThecase: Boolean
|
||||
private val isThisPodInThecase: Boolean
|
||||
get() = rawStatus.isBitSet(6)
|
||||
|
||||
/**
|
||||
* The data flip bit is set if the left pod is primary.
|
||||
* For the pod that is in the case, this is flipped again though.
|
||||
*/
|
||||
override val isLeftPodMicrophone: Boolean
|
||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||
override val isLeftPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(3)
|
||||
false -> rawStatus.isBitSet(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* The data flip bit is UNset if the right pod is primary.
|
||||
* For the pod that is in the case, this is flipped again though.
|
||||
*/
|
||||
override val isRightPodMicrophone: Boolean
|
||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||
override val isRightPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(1)
|
||||
false -> rawStatus.isBitSet(3)
|
||||
}
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = when (val value = rawCaseBattery.toInt()) {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.common.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
@@ -31,10 +30,26 @@ data class FakeAirPodsPro constructor(
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
|
||||
) : ApplePods, DualPodDevice, HasChargeDetectionDual, HasEarDetectionDual, HasCase {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO
|
||||
|
||||
@get:DrawableRes
|
||||
override val iconRes: Int
|
||||
get() = R.drawable.devic_airpods_pro2_both
|
||||
|
||||
@get:DrawableRes
|
||||
override val caseIcon: Int
|
||||
get() = R.drawable.devic_airpods_pro2_case
|
||||
|
||||
@get:DrawableRes
|
||||
override val leftPodIcon: Int
|
||||
get() = R.drawable.devic_airpods_pro2_left
|
||||
|
||||
@get:DrawableRes
|
||||
override val rightPodIcon: Int
|
||||
get() = R.drawable.devic_airpods_pro2_right
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||
|
||||
@@ -79,22 +94,32 @@ data class FakeAirPodsPro constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val isThisPodInThecase: Boolean
|
||||
override val isLeftPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(0)
|
||||
true -> rawFlags.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(1)
|
||||
true -> rawFlags.isBitSet(0)
|
||||
}
|
||||
|
||||
private val isThisPodInThecase: Boolean
|
||||
get() = rawStatus.isBitSet(6)
|
||||
|
||||
/**
|
||||
* The data flip bit is set if the left pod is primary.
|
||||
* For the pod that is in the case, this is flipped again though.
|
||||
*/
|
||||
override val isLeftPodMicrophone: Boolean
|
||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||
override val isLeftPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(3)
|
||||
false -> rawStatus.isBitSet(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* The data flip bit is UNset if the right pod is primary.
|
||||
* For the pod that is in the case, this is flipped again though.
|
||||
*/
|
||||
override val isRightPodMicrophone: Boolean
|
||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||
override val isRightPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(1)
|
||||
false -> rawStatus.isBitSet(3)
|
||||
}
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = when (val value = rawCaseBattery.toInt()) {
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.common.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* AirPods Pro2 clone
|
||||
* https://discord.com/channels/548521543039189022/927235844127993866/1060911213539774504
|
||||
*/
|
||||
data class FakeAirPodsPro2 constructor(
|
||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||
override val seenLastAt: Instant = Instant.now(),
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null,
|
||||
) : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO2
|
||||
|
||||
@get:DrawableRes
|
||||
override val iconRes: Int
|
||||
get() = R.drawable.devic_airpods_pro2_both
|
||||
|
||||
@get:DrawableRes
|
||||
override val caseIcon: Int
|
||||
get() = R.drawable.devic_airpods_pro2_case
|
||||
|
||||
@get:DrawableRes
|
||||
override val leftPodIcon: Int
|
||||
get() = R.drawable.devic_airpods_pro2_left
|
||||
|
||||
@get:DrawableRes
|
||||
override val rightPodIcon: Int
|
||||
get() = R.drawable.devic_airpods_pro2_right
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: scanResult.rssi
|
||||
|
||||
private val areValuesFlipped: Boolean
|
||||
get() = !rawStatus.isBitSet(5)
|
||||
|
||||
override val batteryLeftPodPercent: Float?
|
||||
get() {
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.upperNibble.toInt()
|
||||
false -> rawPodsBattery.lowerNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Left pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
(value / 10f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val batteryRightPodPercent: Float?
|
||||
get() {
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.lowerNibble.toInt()
|
||||
false -> rawPodsBattery.upperNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Right pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val isThisPodInThecase: Boolean
|
||||
get() = rawStatus.isBitSet(6)
|
||||
|
||||
override val isLeftPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(3)
|
||||
false -> rawStatus.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(1)
|
||||
false -> rawStatus.isBitSet(3)
|
||||
}
|
||||
|
||||
override val isLeftPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(0)
|
||||
true -> rawFlags.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(1)
|
||||
true -> rawFlags.isBitSet(0)
|
||||
}
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = when (val value = rawCaseBattery.toInt()) {
|
||||
15 -> cachedBatteryPercentage
|
||||
else -> if (value > 10) {
|
||||
log { "Case: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
|
||||
override val isCaseCharging: Boolean
|
||||
get() = rawFlags.isBitSet(2)
|
||||
|
||||
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsPro2>(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||
getModelInfo().full == DEVICE_CODE && length == 19
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = FakeAirPodsPro2(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenLastAt = scanResult.receivedAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEVICE_CODE = 0x1420.toUShort()
|
||||
private val TAG = logTag("PodDevice", "Apple", "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.pods.core.HasEarDetection
|
||||
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 kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
@@ -69,7 +69,7 @@ class AutoConnect @Inject constructor(
|
||||
val conditionFulfilled = when (condition) {
|
||||
AutoConnectCondition.WHEN_SEEN -> true
|
||||
AutoConnectCondition.CASE_OPEN -> when (mainDevice) {
|
||||
is DualAirPods -> mainDevice.caseLidState == DualAirPods.LidState.OPEN
|
||||
is DualApplePods -> mainDevice.caseLidState == DualApplePods.LidState.OPEN
|
||||
else -> true
|
||||
}
|
||||
AutoConnectCondition.IN_EAR -> when (mainDevice) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package eu.darken.capod.reaction.core.playpause
|
||||
import eu.darken.capod.common.MediaControl
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
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.common.flow.setupCommonEventHandlers
|
||||
@@ -40,46 +41,51 @@ class PlayPause @Inject constructor(
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.withPrevious()
|
||||
.filter { (previous, current) ->
|
||||
if (previous == null || current == null) return@filter false
|
||||
log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" }
|
||||
val match = previous.identifier == current.identifier
|
||||
if (!match) log(TAG, WARN) { "Main device switched, skipping reaction." }
|
||||
match
|
||||
}
|
||||
.onEach { (previous, current) ->
|
||||
if (previous is HasEarDetection && current is HasEarDetection) {
|
||||
log(TAG, VERBOSE) { "previous=${previous.isBeingWorn}, current=${current.isBeingWorn}" }
|
||||
log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" }
|
||||
if (previous.identifier != current.identifier) {
|
||||
return@onEach
|
||||
}
|
||||
log(TAG, VERBOSE) { "Checking\nprevious=$previous\ncurrent=$current" }
|
||||
|
||||
if (previous is HasEarDetectionDual && current is HasEarDetectionDual && reactionSettings.onePodMode.value) {
|
||||
log(TAG) { "Ear status changed for dual pod device in single pod mode." }
|
||||
val previousWorn = previous.isEitherPodInEar
|
||||
val currentWorn = current.isEitherPodInEar
|
||||
if (!previousWorn && currentWorn && !mediaControl.isPlaying) {
|
||||
if (reactionSettings.autoPlay.value) {
|
||||
mediaControl.sendPlay()
|
||||
} else {
|
||||
log(TAG) { "autoPlay is disabled" }
|
||||
}
|
||||
} else if (!currentWorn && previousWorn && mediaControl.isPlaying) {
|
||||
if (reactionSettings.autoPause.value) {
|
||||
mediaControl.sendPause()
|
||||
} else {
|
||||
log(TAG) { "autoPause is disabled" }
|
||||
}
|
||||
}
|
||||
} else if (previous.isBeingWorn != current.isBeingWorn) {
|
||||
log(TAG) { "Ear status changed for monitored device." }
|
||||
if (current.isBeingWorn && !mediaControl.isPlaying) {
|
||||
if (reactionSettings.autoPlay.value) {
|
||||
mediaControl.sendPlay()
|
||||
} else {
|
||||
log(TAG) { "autoPlay is disabled" }
|
||||
}
|
||||
} else if (!current.isBeingWorn && mediaControl.isPlaying) {
|
||||
if (reactionSettings.autoPause.value) {
|
||||
mediaControl.sendPause()
|
||||
} else {
|
||||
log(TAG) { "autoPause is disabled" }
|
||||
}
|
||||
}
|
||||
val previousWorn: Boolean?
|
||||
val currentWorn: Boolean?
|
||||
|
||||
when {
|
||||
reactionSettings.onePodMode.value && previous is HasEarDetectionDual && current is HasEarDetectionDual -> {
|
||||
previousWorn = previous.isEitherPodInEar
|
||||
currentWorn = current.isEitherPodInEar
|
||||
log(TAG, VERBOSE) { "previous: left=${previous.isLeftPodInEar}, right=${previous.isRightPodInEar}" }
|
||||
log(TAG, VERBOSE) { "current: left${current.isLeftPodInEar}, right=${current.isRightPodInEar}" }
|
||||
}
|
||||
previous is HasEarDetection && current is HasEarDetection -> {
|
||||
previousWorn = previous.isBeingWorn
|
||||
currentWorn = current.isBeingWorn
|
||||
log(TAG, VERBOSE) { "prev.isBeingWorn=${previousWorn}, cur.isBeingWorn=${currentWorn}" }
|
||||
}
|
||||
else -> {
|
||||
log(TAG, VERBOSE) { "Current devices don't support ear detection." }
|
||||
previousWorn = null
|
||||
currentWorn = null
|
||||
}
|
||||
}
|
||||
|
||||
if (previousWorn == false && currentWorn == true && !mediaControl.isPlaying) {
|
||||
if (reactionSettings.autoPlay.value) {
|
||||
log(TAG) { "autoPlay is triggered, sendPlay()" }
|
||||
mediaControl.sendPlay()
|
||||
} else {
|
||||
log(TAG, VERBOSE) { "autoPlay is disabled" }
|
||||
}
|
||||
} else if (previousWorn == true && currentWorn == false && mediaControl.isPlaying) {
|
||||
if (reactionSettings.autoPause.value) {
|
||||
log(TAG) { "autoPause is triggered, sendPause()" }
|
||||
mediaControl.sendPause()
|
||||
} else {
|
||||
log(TAG) { "autoPause is disabled" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import eu.darken.capod.common.flow.withPrevious
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
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 kotlinx.coroutines.flow.*
|
||||
import java.time.Duration
|
||||
@@ -34,7 +34,7 @@ class PopUpReaction @Inject constructor(
|
||||
.withPrevious()
|
||||
.setupCommonEventHandlers(TAG) { "monitor" }
|
||||
.mapNotNull { (previous, current) ->
|
||||
if (previous !is DualAirPods? || current !is DualAirPods) {
|
||||
if (previous !is DualApplePods? || current !is DualApplePods) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
log(TAG, VERBOSE) {
|
||||
@@ -57,8 +57,8 @@ class PopUpReaction @Inject constructor(
|
||||
tryPopWindow(current)
|
||||
}
|
||||
|
||||
private suspend fun tryPopWindow(current: DualAirPods): Event? = when {
|
||||
current.caseLidState == DualAirPods.LidState.OPEN -> {
|
||||
private suspend fun tryPopWindow(current: DualApplePods): Event? = when {
|
||||
current.caseLidState == DualApplePods.LidState.OPEN -> {
|
||||
log(TAG, INFO) { "Show popup" }
|
||||
|
||||
val now = Instant.now()
|
||||
@@ -74,9 +74,9 @@ class PopUpReaction @Inject constructor(
|
||||
null
|
||||
}
|
||||
}
|
||||
current.caseLidState != DualAirPods.LidState.OPEN -> {
|
||||
current.caseLidState != DualApplePods.LidState.OPEN -> {
|
||||
when (current.caseLidState) {
|
||||
DualAirPods.LidState.CLOSED -> {
|
||||
DualApplePods.LidState.CLOSED -> {
|
||||
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
||||
coolDowns.remove(current.identifier)
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<string name="app_name_pro">CAPod Pro</string>
|
||||
<string name="general_value_not_available_label">Δε διατίθεται</string>
|
||||
<string name="general_error_label">Σφάλμα</string>
|
||||
<string name="general_grant_permission_action">Παραχώρηση άδειας</string>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<string name="app_name">CAPod</string>
|
||||
<string name="app_name_pro">CAPod Pro</string>
|
||||
<string name="general_error_label">Erro</string>
|
||||
<string name="general_grant_permission_action">Conceder permissão</string>
|
||||
<string name="overview_nomaindevice_label">Nenhum dispositivo principal</string>
|
||||
@@ -10,6 +12,7 @@
|
||||
<string name="permission_bluetooth_connect_description">Este aplicativo requer a permissão \"Conexão Bluetooth\" para interagir com dispositivos emparelhados e iniciar conexões.</string>
|
||||
<string name="permission_bluetooth_scan_label">Escaneando bluetooth</string>
|
||||
<string name="permission_bluetooth_scan_description">A permissão \"Escaneamento de Bluetooth\" permite que este aplicativo descubra e receba dados de Bluetooth de dispositivos próximos, como seus AirPods.</string>
|
||||
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||
<string name="permission_bluetooth_description">Este aplicativo requer a permissão \"Bluetooth\" para se conectar a dispositivos bluetooth emparelhados.</string>
|
||||
<string name="permission_access_fine_location_label">Acesse a boa localização</string>
|
||||
<string name="permission_access_fine_location_description">O CAPod usa a permissão \"Boa localização\" para receber dados de Bluetooth Low Energy. Seus fones de ouvido usam a tecnologia Bluetooth Low Energy para transmitir seu status. Este aplicativo NÃO usará dados Bluetooth para determinar sua localização.</string>
|
||||
|
||||
@@ -14,14 +14,14 @@ class AirPodsFactoryTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
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>()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
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>()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.pods.core.apple.airpods.HasStateDetectionAirPods
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -8,7 +9,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
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()
|
||||
rawDeviceModel shouldBe 0x0e20.toUShort()
|
||||
@@ -24,13 +25,13 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
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
|
||||
// --^-----
|
||||
isLeftPodMicrophone shouldBe true
|
||||
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
|
||||
// --^-----
|
||||
isLeftPodMicrophone shouldBe false
|
||||
@@ -41,24 +42,24 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `test AirPodDevice - left pod ear status`() = runTest {
|
||||
// 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
|
||||
// 765432¹0
|
||||
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
|
||||
// 765432¹0
|
||||
isLeftPodInEar shouldBe false
|
||||
}
|
||||
|
||||
// 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
|
||||
// 7654³210
|
||||
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
|
||||
// 7654³210
|
||||
isLeftPodInEar shouldBe false
|
||||
@@ -68,24 +69,24 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `test AirPodDevice - right pod ear status`() = runTest {
|
||||
// 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
|
||||
// 7654³210
|
||||
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
|
||||
// 7654³210
|
||||
isRightPodInEar shouldBe false
|
||||
}
|
||||
|
||||
// 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
|
||||
// 765432¹0
|
||||
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
|
||||
// 765432¹0
|
||||
isRightPodInEar shouldBe false
|
||||
@@ -95,13 +96,13 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `test AirPodDevice - battery status`() = runTest {
|
||||
// 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
|
||||
batteryLeftPodPercent shouldBe 0.9f
|
||||
batteryRightPodPercent shouldBe 0.8f
|
||||
}
|
||||
// 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
|
||||
batteryLeftPodPercent shouldBe 0.9f
|
||||
batteryRightPodPercent shouldBe 0.8f
|
||||
@@ -114,13 +115,13 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
* Right pod is charging
|
||||
*/
|
||||
// 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
|
||||
isLeftPodCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
}
|
||||
// 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
|
||||
isLeftPodCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
@@ -130,26 +131,26 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
* Left pod is charging
|
||||
*/
|
||||
// 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
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe false
|
||||
}
|
||||
// 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
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe false
|
||||
}
|
||||
|
||||
// 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
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
}
|
||||
// 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
|
||||
isLeftPodCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
@@ -158,11 +159,11 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
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
|
||||
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
|
||||
isCaseCharging shouldBe true
|
||||
}
|
||||
@@ -171,75 +172,75 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `test AirPodDevice - case lid test`() = runTest {
|
||||
// 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
|
||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
||||
caseLidState shouldBe DualApplePods.LidState.OPEN
|
||||
}
|
||||
// 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
|
||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
||||
caseLidState shouldBe DualApplePods.LidState.OPEN
|
||||
}
|
||||
// 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
|
||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
||||
caseLidState shouldBe DualApplePods.LidState.OPEN
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
||||
caseLidState shouldBe DualApplePods.LidState.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
|
||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
||||
caseLidState shouldBe DualApplePods.LidState.CLOSED
|
||||
}
|
||||
// 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
|
||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
||||
caseLidState shouldBe DualApplePods.LidState.CLOSED
|
||||
}
|
||||
// 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
|
||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
||||
caseLidState shouldBe DualApplePods.LidState.CLOSED
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - connection state`() = runTest {
|
||||
// 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
|
||||
state shouldBe DualAirPods.ConnectionState.DISCONNECTED
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.DISCONNECTED
|
||||
}
|
||||
// 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
|
||||
state shouldBe DualAirPods.ConnectionState.IDLE
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
|
||||
}
|
||||
// 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
|
||||
state shouldBe DualAirPods.ConnectionState.MUSIC
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.MUSIC
|
||||
}
|
||||
// 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
|
||||
state shouldBe DualAirPods.ConnectionState.CALL
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.CALL
|
||||
}
|
||||
// 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
|
||||
state shouldBe DualAirPods.ConnectionState.RINGING
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.RINGING
|
||||
}
|
||||
// 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
|
||||
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.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 io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -35,9 +35,7 @@ class AirPodsGen1Test : BaseAirPodsTest() {
|
||||
isRightPodInEar shouldBe false
|
||||
batteryCasePercent shouldBe 0.6f
|
||||
|
||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
||||
|
||||
state shouldBe DualAirPods.ConnectionState.DISCONNECTED
|
||||
caseLidState shouldBe DualApplePods.LidState.OPEN
|
||||
|
||||
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.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 io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -34,9 +34,9 @@ class AirPodsGen2Test : BaseAirPodsTest() {
|
||||
isRightPodInEar shouldBe true
|
||||
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
|
||||
|
||||
|
||||
@@ -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.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 io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -34,9 +34,9 @@ class AirPodsGen3Test : BaseAirPodsTest() {
|
||||
isRightPodInEar shouldBe false
|
||||
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
|
||||
|
||||
@@ -58,9 +58,9 @@ class AirPodsGen3Test : BaseAirPodsTest() {
|
||||
isRightPodInEar shouldBe true
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsFitProTest : BaseAirPodsTest() {
|
||||
|
||||
/**
|
||||
* From https://github.com/d4rken-org/capod/issues/33#issuecomment-1256235651
|
||||
*/
|
||||
@Test
|
||||
fun `test basics`() = runTest {
|
||||
create<BeatsFitPro>("07 19 01 12 20 20 FA 8F 01 11 24 9B 9B 23 52 60 5A 8C 32 1C A5 C2 81 51 82 AF C8") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1220.toUShort()
|
||||
rawStatus shouldBe 0x20.toUByte()
|
||||
rawPodsBattery shouldBe 0xFA.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0xF.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x11.toUByte()
|
||||
rawSuffix shouldBe 0x24.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe null
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.UNKNOWN.name
|
||||
|
||||
model shouldBe PodDevice.Model.BEATS_FIT_PRO
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@ class FakeAirPodsGen1Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `charging in box`() = runTest {
|
||||
create<FakeAirPodsGen1>("07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00") {
|
||||
create<FakeAirPodsGen2>("07 13 01 0F 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0220.toUShort()
|
||||
rawDeviceModel shouldBe 0x0F20.toUShort()
|
||||
rawStatus shouldBe 0x71.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0x3.toUShort()
|
||||
@@ -24,11 +24,17 @@ class FakeAirPodsGen1Test : BaseAirPodsTest() {
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isLeftPodInEar shouldBe false
|
||||
isRightPodInEar shouldBe false
|
||||
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
|
||||
batteryCasePercent shouldBe 0.7f
|
||||
|
||||
model shouldBe PodDevice.Model.FAKE_AIRPODS_GEN1
|
||||
model shouldBe PodDevice.Model.FAKE_AIRPODS_GEN2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class FakeAirPodsGen2Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `charging in box`() = runTest {
|
||||
create<FakeAirPodsGen1>("07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0220.toUShort()
|
||||
rawStatus shouldBe 0x71.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0x3.toUShort()
|
||||
rawCaseBattery shouldBe 0x7.toUShort()
|
||||
rawCaseLidState shouldBe 0x32.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x10.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isLeftPodInEar shouldBe false
|
||||
isRightPodInEar shouldBe false
|
||||
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
|
||||
batteryCasePercent shouldBe 0.7f
|
||||
|
||||
model shouldBe PodDevice.Model.FAKE_AIRPODS_GEN1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,9 @@ class FakeAirPodsGen3Test : BaseAirPodsTest() {
|
||||
batteryLeftPodPercent shouldBe 1f
|
||||
batteryRightPodPercent shouldBe 1f
|
||||
|
||||
isLeftPodInEar shouldBe false
|
||||
isRightPodInEar shouldBe false
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class FakeAirPodsPro2Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `guessed data`() = runTest {
|
||||
create<FakeAirPodsPro2>("07 13 01 14 20 75 AA 58 35 00 10 00 E4 E4 26 00 00 00 00 00 00") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1420.toUShort()
|
||||
rawStatus shouldBe 0x75.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0x5.toUShort()
|
||||
rawCaseBattery shouldBe 0x8.toUShort()
|
||||
rawCaseLidState shouldBe 0x35.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x10.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe true
|
||||
|
||||
batteryCasePercent shouldBe 0.8f
|
||||
|
||||
model shouldBe PodDevice.Model.FAKE_AIRPODS_PRO2
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* https://discord.com/channels/548521543039189022/927235844127993866/1063027552765087754
|
||||
*/
|
||||
@Test
|
||||
fun `user supplied`() = runTest {
|
||||
create<FakeAirPodsPro2>("07 13 01 14 20 75 AA 72 39 00 00 6F E4 E4 93 30 00 30 30 30 30") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1420.toUShort()
|
||||
rawStatus shouldBe 0x75.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0x7.toUShort()
|
||||
rawCaseBattery shouldBe 0x2.toUShort()
|
||||
rawCaseLidState shouldBe 0x39.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x0.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe true
|
||||
|
||||
batteryCasePercent shouldBe 0.2f
|
||||
|
||||
model shouldBe PodDevice.Model.FAKE_AIRPODS_PRO2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,12 @@ class FakeAirPodsProTest : BaseAirPodsTest() {
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
|
||||
isLeftPodInEar shouldBe false
|
||||
isRightPodInEar shouldBe false
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
|
||||
batteryCasePercent shouldBe 0.7f
|
||||
|
||||
@@ -8,7 +8,7 @@ import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsDualItemBinding
|
||||
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
|
||||
|
||||
class DualPodsCardVH(parent: ViewGroup) :
|
||||
@@ -32,7 +32,7 @@ class DualPodsCardVH(parent: ViewGroup) :
|
||||
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
|
||||
else setTypeface(null, Typeface.NORMAL)
|
||||
|
||||
if (device is DualAirPods && item.showDebug) {
|
||||
if (device is DualApplePods && item.showDebug) {
|
||||
append(" [${device.primaryPod.name}]")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsDualItemBinding
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@@ -34,7 +34,7 @@ class DualPodsCardVH(parent: ViewGroup) :
|
||||
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
|
||||
else setTypeface(null, Typeface.NORMAL)
|
||||
|
||||
if (device is DualAirPods && item.showDebug) {
|
||||
if (device is DualApplePods && item.showDebug) {
|
||||
append(" [${device.primaryPod.name}]")
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ class DualPodsCardVH(parent: ViewGroup) :
|
||||
|
||||
// Case lid state
|
||||
device.apply {
|
||||
if (this is DualAirPods) {
|
||||
if (this is DualApplePods) {
|
||||
podCaseLidLabel.text = when (caseLidState) {
|
||||
LidState.OPEN -> context.getString(R.string.pods_case_status_open_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 eu.darken.capod.R
|
||||
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 javax.inject.Inject
|
||||
|
||||
@@ -16,12 +16,12 @@ class MonitorNotificationViewFactory @Inject constructor(
|
||||
) {
|
||||
|
||||
fun createContentView(device: PodDevice): RemoteViews = when (device) {
|
||||
is DualAirPods -> createDualApplePods(device)
|
||||
is DualApplePods -> createDualApplePods(device)
|
||||
is SingleApplePods -> createSingleApplePods(device)
|
||||
else -> createUnknownDevice(device)
|
||||
}
|
||||
|
||||
private fun createDualApplePods(device: DualAirPods): RemoteViews = RemoteViews(
|
||||
private fun createDualApplePods(device: DualApplePods): RemoteViews = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.monitor_notification_dual_pods_small
|
||||
).apply {
|
||||
|
||||
@@ -12,7 +12,7 @@ import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.databinding.PopupNotificationDualPodsBinding
|
||||
import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding
|
||||
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 javax.inject.Inject
|
||||
|
||||
@@ -26,13 +26,13 @@ class PopUpPodViewFactory @Inject constructor(
|
||||
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
|
||||
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?
|
||||
is SingleApplePods -> createSingleApplePods(parent, 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 {
|
||||
device.apply {
|
||||
podIcon.setImageResource(iconRes)
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:textStyle="bold"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="AirPods Max" />
|
||||
|
||||
<LinearLayout
|
||||
@@ -24,7 +24,7 @@
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_icon"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/devic_airpods_gen1_left"
|
||||
android:src="@drawable/devic_airpods_pro2_left"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
@@ -58,7 +58,7 @@
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_icon"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/devic_airpods_gen1_right"
|
||||
android:src="@drawable/devic_airpods_pro2_right"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### Updated by release.sh ###
|
||||
project.versioning.major=2
|
||||
project.versioning.minor=5
|
||||
project.versioning.patch=0
|
||||
project.versioning.build=1
|
||||
project.versioning.minor=6
|
||||
project.versioning.patch=1
|
||||
project.versioning.build=0
|
||||
#############################
|
||||
|
||||