Skip to content

Commit

Permalink
More granular pod features (#76)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
d4rken authored Jan 18, 2023
1 parent 0cdac10 commit 15141c8
Show file tree
Hide file tree
Showing 24 changed files with 152 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
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
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)) {
Expand Down Expand Up @@ -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)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,7 +24,7 @@ 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

Expand All @@ -51,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) {

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,15 +20,15 @@ data class BeatsFitPro(
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.BEATS_FIT_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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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.DualAirPods
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
Expand All @@ -30,7 +30,7 @@ data class FakeAirPodsPro2 constructor(
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualAirPods.LidState? = null,
private val cachedCaseState: DualApplePods.LidState? = null,
) : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase {

override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 15141c8

Please sign in to comment.