Cache case battery level

This commit is contained in:
darken
2022-01-05 09:00:42 +01:00
parent 490aebf5e0
commit 368298c288
18 changed files with 105 additions and 83 deletions
@@ -5,7 +5,7 @@ import android.view.ViewGroup
import eu.darken.capod.R
import eu.darken.capod.common.lists.binding
import eu.darken.capod.databinding.OverviewPodsAppleDualItemBinding
import eu.darken.capod.pods.core.DualPodDevice.Pod
import eu.darken.capod.pods.core.HasDualPods.Pod
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePods.DeviceColor
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
@@ -0,0 +1,9 @@
package eu.darken.capod.pods.core
interface HasCase {
val batteryCasePercent: Float?
val isCaseCharging: Boolean
}
@@ -1,6 +1,6 @@
package eu.darken.capod.pods.core
interface DualPodDevice : PodDevice {
interface HasDualPods {
enum class Pod {
LEFT,
@@ -10,6 +10,4 @@ interface DualPodDevice : PodDevice {
val batteryLeftPodPercent: Float?
val batteryRightPodPercent: Float?
val batteryCasePercent: Float?
}
@@ -1,6 +1,6 @@
package eu.darken.capod.pods.core
interface SinglePodDevice : PodDevice {
interface HasSinglePod {
val batteryHeadsetPercent: Float?
}
@@ -4,14 +4,18 @@ import android.content.Context
import eu.darken.capod.R
import kotlin.math.roundToInt
fun DualPodDevice.getBatteryLevelLeftPod(context: Context): String =
fun HasDualPods.getBatteryLevelLeftPod(context: Context): String =
batteryLeftPodPercent?.let { "${(it * 100).roundToInt()}%" }
?: context.getString(R.string.general_value_not_available_label)
fun DualPodDevice.getBatteryLevelRightPod(context: Context): String =
fun HasDualPods.getBatteryLevelRightPod(context: Context): String =
batteryRightPodPercent?.let { "${(it * 100).roundToInt()}%" }
?: context.getString(R.string.general_value_not_available_label)
fun DualPodDevice.getBatteryLevelCase(context: Context): String =
fun HasCase.getBatteryLevelCase(context: Context): String =
batteryCasePercent?.let { "${(it * 100).roundToInt()}%" }
?: context.getString(R.string.general_value_not_available_label)
fun HasSinglePod.getBatteryLevelHeadset(context: Context): String =
batteryHeadsetPercent?.let { "${(it * 100).roundToInt()}%" }
?: context.getString(R.string.general_value_not_available_label)
@@ -1,9 +0,0 @@
package eu.darken.capod.pods.core
import android.content.Context
import eu.darken.capod.R
import kotlin.math.roundToInt
fun SinglePodDevice.getBatteryLevelHeadset(context: Context): String =
batteryHeadsetPercent?.let { "${(it * 100).roundToInt()}%" }
?: context.getString(R.string.general_value_not_available_label)
@@ -7,6 +7,7 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.*
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.airpods.*
import eu.darken.capod.pods.core.apple.beats.*
@@ -45,8 +46,14 @@ class AppleFactory @Inject constructor(
}
}
private val lock = Mutex()
private val knownDevs = mutableMapOf<UUID, KnownDevice>()
private val mutex = Mutex()
data class ValueCache(
val caseBatteryPercentage: Float?
)
private val cachedValues = mutableMapOf<UUID, ValueCache>()
private suspend fun getMessage(scanResult: ScanResult): ProximityPairing.Message? {
val messages = try {
@@ -140,7 +147,7 @@ class AppleFactory @Inject constructor(
return identifier!!
}
suspend fun create(scanResult: ScanResult): PodDevice? = mutex.withLock {
suspend fun create(scanResult: ScanResult): PodDevice? = lock.withLock {
val pm = getMessage(scanResult) ?: return@withLock null
val identifier = recognizeDevice(scanResult, pm)
@@ -155,39 +162,49 @@ class AppleFactory @Inject constructor(
"Decoding (MAC=${scanResult.device.address}, nanos=${scanResult.timestampNanos}, rssi=${scanResult.rssi}): $dataHex"
}
return createPodDevice(scanResult, pm, identifier)
return createSpecificDevice(scanResult, pm, identifier)
}
private fun createPodDevice(
private fun createSpecificDevice(
scanResult: ScanResult,
pm: ProximityPairing.Message,
identifier: UUID
): ApplePods {
val dm = (
((pm.data[1].toInt() and 255) shl 8) or (pm.data[2].toInt() and 255)
).toUShort()
val cachedCaseBattery = cachedValues[identifier]?.caseBatteryPercentage
val dm = (((pm.data[1].toInt() and 255) shl 8) or (pm.data[2].toInt() and 255)).toUShort()
val dmDirty = pm.data[1]
return when {
val specificDevice = when {
dm == 0x0220.toUShort() -> AirPodsGen1(
identifier = identifier,
scanResult = scanResult,
proximityMessage = pm
proximityMessage = pm,
cachedBatteryPercentage = cachedCaseBattery,
)
dm == 0x0F20.toUShort() -> AirPodsGen2(
identifier = identifier,
scanResult = scanResult,
proximityMessage = pm
proximityMessage = pm,
cachedBatteryPercentage = cachedCaseBattery,
)
dm == 0x0e20.toUShort() -> AirPodsPro(
identifier = identifier,
scanResult = scanResult,
proximityMessage = pm
proximityMessage = pm,
cachedBatteryPercentage = cachedCaseBattery,
)
dmDirty == 11.toUByte() -> PowerBeatsPro(
identifier = identifier,
scanResult = scanResult,
proximityMessage = pm,
cachedBatteryPercentage = cachedCaseBattery,
)
dmDirty == 10.toUByte() -> AirPodsMax(
identifier = identifier,
scanResult = scanResult,
proximityMessage = pm
proximityMessage = pm,
)
dm == 0x1320.toUShort() -> PowerBeats3(
identifier = identifier,
@@ -204,11 +221,6 @@ class AppleFactory @Inject constructor(
scanResult = scanResult,
proximityMessage = pm
)
dmDirty == 11.toUByte() -> PowerBeatsPro(
identifier = identifier,
scanResult = scanResult,
proximityMessage = pm
)
dm == 0x0520.toUShort() -> BeatsX(
identifier = identifier,
scanResult = scanResult,
@@ -230,6 +242,12 @@ class AppleFactory @Inject constructor(
)
}
}
cachedValues[identifier] = ValueCache(
caseBatteryPercentage = (specificDevice as? HasCase)?.batteryCasePercent
)
return specificDevice
}
companion object {
@@ -2,9 +2,9 @@ package eu.darken.capod.pods.core.apple
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.pods.core.SinglePodDevice
import eu.darken.capod.pods.core.HasSinglePod
interface BasicSingleApplePods : ApplePods, SinglePodDevice {
interface BasicSingleApplePods : ApplePods, HasSinglePod {
override val batteryHeadsetPercent: Float?
get() = when (val value = rawPodsBattery.lowerNibble.toInt()) {
@@ -7,11 +7,12 @@ 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.DualPodDevice
import eu.darken.capod.pods.core.DualPodDevice.Pod
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasDualPods
import eu.darken.capod.pods.core.HasDualPods.Pod
import eu.darken.capod.pods.core.HasEarDetection
interface DualApplePods : ApplePods, DualPodDevice, HasEarDetection {
interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
val microPhonePod: Pod
get() = when (rawStatus.isBitSet(5)) {
@@ -53,17 +54,6 @@ interface DualApplePods : ApplePods, DualPodDevice, HasEarDetection {
}
}
override val batteryCasePercent: Float?
get() = when (val value = rawCaseBattery.lowerNibble.toInt()) {
15 -> null
else -> if (value > 10) {
log(tag) { "Case: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
val isLeftPodInEar: Boolean
get() = when (microPhonePod) {
Pod.LEFT -> rawStatus.isBitSet(1)
@@ -79,9 +69,6 @@ interface DualApplePods : ApplePods, DualPodDevice, HasEarDetection {
override val isBeingWorn: Boolean
get() = isLeftPodInEar && isRightPodInEar
val isCaseCharging: Boolean
get() = rawCaseBattery.upperNibble.isBitSet(2)
val isLeftPodCharging: Boolean
get() = when (microPhonePod) {
Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(0)
@@ -94,6 +81,20 @@ interface DualApplePods : ApplePods, DualPodDevice, HasEarDetection {
Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(0)
}
override val batteryCasePercent: Float?
get() = when (val value = rawCaseBattery.lowerNibble.toInt()) {
15 -> null
else -> if (value > 10) {
log(tag) { "Case: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
override val isCaseCharging: Boolean
get() = rawCaseBattery.upperNibble.isBitSet(2)
val caseLidState: LidState
get() = LidState.values().firstOrNull { it.raw == rawCaseLidState } ?: LidState.UNKNOWN
@@ -1,30 +1,15 @@
package eu.darken.capod.pods.core.apple
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.isBitSet
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.HasSinglePod
interface SingleApplePods : BasicSingleApplePods, HasEarDetection {
val batteryCasePercent: Float?
get() = when (val value = rawCaseBattery.lowerNibble.toInt()) {
15 -> null
else -> if (value > 10) {
log(tag) { "Case: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
interface SingleApplePods : BasicSingleApplePods, HasEarDetection, HasSinglePod {
val isHeadphonesBeingWorn: Boolean
get() = rawStatus.isBitSet(1)
val isCaseCharging: Boolean
get() = rawCaseBattery.upperNibble.isBitSet(2)
val isHeadsetBeingCharged: Boolean
get() = rawCaseBattery.upperNibble.isBitSet(0)
@@ -13,7 +13,8 @@ data class AirPodsGen1 constructor(
override val identifier: UUID = UUID.randomUUID(),
override val lastSeenAt: Instant = Instant.now(),
override val scanResult: ScanResult,
override val proximityMessage: ProximityPairing.Message
override val proximityMessage: ProximityPairing.Message,
private val cachedBatteryPercentage: Float?
) : DualApplePods {
override fun getLabel(context: Context): String = "AirPods (Gen 1)"
@@ -22,4 +23,7 @@ data class AirPodsGen1 constructor(
get() = R.drawable.ic_device_airpods_gen1
override val tag: String = logTag("Pod", "Apple", "AirPods", "Gen1")
override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage
}
@@ -13,7 +13,8 @@ data class AirPodsGen2 constructor(
override val identifier: UUID = UUID.randomUUID(),
override val lastSeenAt: Instant = Instant.now(),
override val scanResult: ScanResult,
override val proximityMessage: ProximityPairing.Message
override val proximityMessage: ProximityPairing.Message,
private val cachedBatteryPercentage: Float?,
) : DualApplePods {
override fun getLabel(context: Context): String = "AirPods (Gen 2)"
@@ -22,4 +23,7 @@ data class AirPodsGen2 constructor(
get() = R.drawable.ic_device_airpods_gen2
override val tag: String = logTag("Pod", "Apple", "AirPods", "Gen2")
override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage
}
@@ -13,7 +13,7 @@ data class AirPodsMax constructor(
override val identifier: UUID = UUID.randomUUID(),
override val lastSeenAt: Instant = Instant.now(),
override val scanResult: ScanResult,
override val proximityMessage: ProximityPairing.Message
override val proximityMessage: ProximityPairing.Message,
) : SingleApplePods {
override fun getLabel(context: Context): String = "AirPods Max"
@@ -13,7 +13,8 @@ data class AirPodsPro constructor(
override val identifier: UUID = UUID.randomUUID(),
override val lastSeenAt: Instant = Instant.now(),
override val scanResult: ScanResult,
override val proximityMessage: ProximityPairing.Message
override val proximityMessage: ProximityPairing.Message,
private val cachedBatteryPercentage: Float?,
) : DualApplePods {
override fun getLabel(context: Context): String {
@@ -24,4 +25,7 @@ data class AirPodsPro constructor(
get() = R.drawable.ic_device_airpods_gen2
override val tag: String = logTag("Pod", "Apple", "AirPods", "Pro")
override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage
}
@@ -13,7 +13,8 @@ data class PowerBeatsPro constructor(
override val identifier: UUID = UUID.randomUUID(),
override val lastSeenAt: Instant = Instant.now(),
override val scanResult: ScanResult,
override val proximityMessage: ProximityPairing.Message
override val proximityMessage: ProximityPairing.Message,
private val cachedBatteryPercentage: Float?,
) : DualApplePods {
override fun getLabel(context: Context): String = "Power Beats Pro"
@@ -22,4 +23,7 @@ data class PowerBeatsPro constructor(
get() = R.drawable.ic_device_generic_earbuds
override val tag: String = logTag("Pod", "Apple", "Beats", "PowerPro")
override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage
}
@@ -1,6 +1,6 @@
package eu.darken.capod.pods.core.apple
import eu.darken.capod.pods.core.DualPodDevice
import eu.darken.capod.pods.core.HasDualPods
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
@@ -27,12 +27,12 @@ class DualApplePodsTest : BaseAirPodsTest() {
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
// --^-----
microPhonePod shouldBe DualPodDevice.Pod.LEFT
microPhonePod shouldBe HasDualPods.Pod.LEFT
}
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
// --^-----
microPhonePod shouldBe DualPodDevice.Pod.RIGHT
microPhonePod shouldBe HasDualPods.Pod.RIGHT
}
}
@@ -1,6 +1,6 @@
package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.DualPodDevice
import eu.darken.capod.pods.core.HasDualPods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualApplePods
import io.kotest.matchers.shouldBe
@@ -22,7 +22,7 @@ class AirPodsProTest : BaseAirPodsTest() {
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x00.toUByte()
microPhonePod shouldBe DualPodDevice.Pod.RIGHT
microPhonePod shouldBe HasDualPods.Pod.RIGHT
batteryLeftPodPercent shouldBe 1.0f
batteryRightPodPercent shouldBe 1.0f
@@ -39,7 +39,7 @@ class AirPodsProTest : BaseAirPodsTest() {
@Test
fun `test AirPods from my downstairs neighbour`() = runBlockingTest {
create<AirPodsPro>("07 19 01 0E 20 00 F3 8F 02 00 04 79 C6 3F F9 C3 15 D9 11 A1 3C B1 58 66 B9 8B 67") {
microPhonePod shouldBe DualPodDevice.Pod.RIGHT
microPhonePod shouldBe HasDualPods.Pod.RIGHT
batteryLeftPodPercent shouldBe null
batteryRightPodPercent shouldBe 0.3f
@@ -1,6 +1,6 @@
package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.DualPodDevice
import eu.darken.capod.pods.core.HasDualPods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualApplePods
import io.kotest.matchers.shouldBe
@@ -22,7 +22,7 @@ class PowerBeatsProTest : BaseAirPodsTest() {
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x00.toUByte()
microPhonePod shouldBe DualPodDevice.Pod.RIGHT
microPhonePod shouldBe HasDualPods.Pod.RIGHT
batteryLeftPodPercent shouldBe 1.0f
batteryRightPodPercent shouldBe 1.0f