mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Cache case battery level
This commit is contained in:
+1
-1
@@ -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
-3
@@ -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
-1
@@ -1,6 +1,6 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
interface SinglePodDevice : PodDevice {
|
||||
interface HasSinglePod {
|
||||
|
||||
val batteryHeadsetPercent: Float?
|
||||
}
|
||||
+7
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user