Compare commits

...
11 Commits
33 changed files with 220 additions and 95 deletions
+8 -3
View File
@@ -16,6 +16,7 @@ A companion app that adds support for AirPod specific features to Android:
* Automatically connect phone & AirPods. * Automatically connect phone & AirPods.
* Show popup when case is opened. * Show popup when case is opened.
* Support for Wear OS * Support for Wear OS
* Widgets
CAPod is ad-free. Some additional features require an in-app purchase. CAPod is ad-free. Some additional features require an in-app purchase.
@@ -36,9 +37,13 @@ Currently supported models:
## Download ## Download
* [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | Source | Status |
* [GitHub](https://github.com/d4rken-org/capod/releases/latest) |-----------------------|--------|
* [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.darken.capod) | [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [![](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplayshields.herokuapp.com%2Fplay%3Fi%3Deu.darken.capod%26l%3DAndroid%26m%3D%24version)](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) | [![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/d4rken-org/capod?include_prereleases&label=GitHub)](https://github.com/d4rken-org/capod/releases/latest) |
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [![](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/eu.darken.capod)](https://apt.izzysoft.de/packages/eu.darken.capod/) |
## Support the project ## Support the project
+1 -1
View File
@@ -1 +1 @@
2.3.0-rc0 20300000 2.3.1-rc0 20301000
@@ -12,6 +12,7 @@ import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.replayingShare import eu.darken.capod.common.flow.replayingShare
import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.main.core.PermissionTool import eu.darken.capod.main.core.PermissionTool
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
@@ -79,6 +80,7 @@ class PodMonitor @Inject constructor(
val mainDevice: Flow<PodDevice?> = devices val mainDevice: Flow<PodDevice?> = devices
.map { determineMainDevice(it) } .map { determineMainDevice(it) }
.setupCommonEventHandlers(TAG) { "mainDevice" }
.replayingShare(appScope) .replayingShare(appScope)
private fun createBleScanner() = combine( private fun createBleScanner() = combine(
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple
import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE 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.log
import eu.darken.capod.common.lowerNibble import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.upperNibble import eu.darken.capod.common.upperNibble
@@ -66,7 +67,7 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})" override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
companion object { companion object {
const val MAX_HISTORY = 10 const val MAX_HISTORY = 20
} }
} }
@@ -88,6 +89,22 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
?.caseLidState ?.caseLidState
} }
open fun historyTrimmer(
pods: List<ApplePods>
): List<ApplePods> {
var trimmed = pods.takeLast(KnownDevice.MAX_HISTORY)
// We want to keep the case information
// If both pods are removed, and produce more history, we otherwise lose the case battery info.
val caseInfoFat = pods.lastOrNull { it is HasCase && it.batteryCasePercent != null }
val caseInfoTrimmed = trimmed.lastOrNull { it is HasCase && it.batteryCasePercent != null }
if (caseInfoFat != null && caseInfoTrimmed == null) {
trimmed = listOf(caseInfoFat) + trimmed.takeLast(KnownDevice.MAX_HISTORY - 1)
}
return trimmed
}
internal open fun searchHistory(current: PodType): KnownDevice? { internal open fun searchHistory(current: PodType): KnownDevice? {
val scanResult = current.scanResult val scanResult = current.scanResult
val message = current.proximityMessage val message = current.proximityMessage
@@ -103,7 +120,7 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
.filter { it.history.size > KnownDevice.MAX_HISTORY } .filter { it.history.size > KnownDevice.MAX_HISTORY }
.toList() .toList()
.forEach { .forEach {
knownDevices[it.id] = it.copy(history = it.history.takeLast(KnownDevice.MAX_HISTORY)) knownDevices[it.id] = it.copy(history = historyTrimmer(it.history))
} }
var recognizedDevice: KnownDevice? = knownDevices.values var recognizedDevice: KnownDevice? = knownDevices.values
@@ -118,7 +135,7 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
} }
if (recognizedDevice == null) { if (recognizedDevice == null) {
log(tag) { "searchHistory1: Didn't recognize: $message" } log(tag, WARN) { "searchHistory1: Didn't recognize: $message" }
} }
return recognizedDevice return recognizedDevice
@@ -5,7 +5,7 @@ import com.squareup.moshi.Moshi
import eu.darken.capod.main.core.MonitorMode import eu.darken.capod.main.core.MonitorMode
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import testhelpers.BaseTest import testhelpers.BaseTest
import testhelpers.json.toComparableJson import testhelpers.json.toComparableJson
@@ -25,7 +25,7 @@ class FlowPreferenceMoshiTest : BaseTest() {
) )
@Test @Test
fun `reading and writing using manual reader and writer`() = runBlockingTest { fun `reading and writing using manual reader and writer`() = runTest {
val testData1 = TestGson(string = "teststring") val testData1 = TestGson(string = "teststring")
val testData2 = TestGson(string = "update") val testData2 = TestGson(string = "update")
val moshi = Moshi.Builder().build() val moshi = Moshi.Builder().build()
@@ -67,7 +67,7 @@ class FlowPreferenceMoshiTest : BaseTest() {
} }
@Test @Test
fun `reading and writing using autocreated reader and writer`() = runBlockingTest { fun `reading and writing using autocreated reader and writer`() = runTest {
val testData1 = TestGson(string = "teststring") val testData1 = TestGson(string = "teststring")
val testData2 = TestGson(string = "update") val testData2 = TestGson(string = "update")
val moshi = Moshi.Builder().build() val moshi = Moshi.Builder().build()
@@ -109,7 +109,7 @@ class FlowPreferenceMoshiTest : BaseTest() {
} }
@Test @Test
fun `enum serialization`() = runBlockingTest { fun `enum serialization`() = runTest {
val moshi = Moshi.Builder().build() val moshi = Moshi.Builder().build()
val monitorMode = mockPreferences.createFlowPreference( val monitorMode = mockPreferences.createFlowPreference(
"core.monitor.mode", "core.monitor.mode",
@@ -2,7 +2,7 @@ package eu.darken.capod.common.preferences
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import testhelpers.BaseTest import testhelpers.BaseTest
import testhelpers.preferences.MockSharedPreferences import testhelpers.preferences.MockSharedPreferences
@@ -12,7 +12,7 @@ class FlowPreferenceTest : BaseTest() {
private val mockPreferences = MockSharedPreferences() private val mockPreferences = MockSharedPreferences()
@Test @Test
fun `reading and writing strings`() = runBlockingTest { fun `reading and writing strings`() = runTest {
mockPreferences.createFlowPreference<String?>( mockPreferences.createFlowPreference<String?>(
key = "testKey", key = "testKey",
defaultValue = "default" defaultValue = "default"
@@ -41,7 +41,7 @@ class FlowPreferenceTest : BaseTest() {
} }
@Test @Test
fun `reading and writing boolean`() = runBlockingTest { fun `reading and writing boolean`() = runTest {
mockPreferences.createFlowPreference<Boolean?>( mockPreferences.createFlowPreference<Boolean?>(
key = "testKey", key = "testKey",
defaultValue = true defaultValue = true
@@ -70,7 +70,7 @@ class FlowPreferenceTest : BaseTest() {
} }
@Test @Test
fun `reading and writing long`() = runBlockingTest { fun `reading and writing long`() = runTest {
mockPreferences.createFlowPreference<Long?>( mockPreferences.createFlowPreference<Long?>(
key = "testKey", key = "testKey",
defaultValue = 9000L defaultValue = 9000L
@@ -99,7 +99,7 @@ class FlowPreferenceTest : BaseTest() {
} }
@Test @Test
fun `reading and writing integer`() = runBlockingTest { fun `reading and writing integer`() = runTest {
mockPreferences.createFlowPreference<Long?>( mockPreferences.createFlowPreference<Long?>(
key = "testKey", key = "testKey",
defaultValue = 123 defaultValue = 123
@@ -128,7 +128,7 @@ class FlowPreferenceTest : BaseTest() {
} }
@Test @Test
fun `reading and writing float`() = runBlockingTest { fun `reading and writing float`() = runTest {
mockPreferences.createFlowPreference<Float?>( mockPreferences.createFlowPreference<Float?>(
key = "testKey", key = "testKey",
defaultValue = 3.6f defaultValue = 3.6f
@@ -7,26 +7,27 @@ import eu.darken.capod.pods.core.apple.misc.UnknownAppleDevice
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.instanceOf import io.kotest.matchers.types.instanceOf
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class AirPodsFactoryTest : BaseAirPodsTest() { class AirPodsFactoryTest : BaseAirPodsTest() {
@Test @Test
fun `create AirPodsGen1`() = runBlockingTest { 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<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") {
this shouldBe instanceOf<AirPodsGen1>() this shouldBe instanceOf<AirPodsGen1>()
} }
} }
@Test @Test
fun `create AirPodsPro`() = runBlockingTest { 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<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") {
this shouldBe instanceOf<AirPodsPro>() this shouldBe instanceOf<AirPodsPro>()
} }
} }
@Test @Test
fun `unknown AppleDevice`() = runBlockingTest { fun `unknown AppleDevice`() = runTest {
create<ApplePods>("07 19 01 FF FF 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") { create<ApplePods>("07 19 01 FF FF 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<UnknownAppleDevice>() this shouldBe instanceOf<UnknownAppleDevice>()
} }
@@ -1,13 +1,13 @@
package eu.darken.capod.pods.core.apple package eu.darken.capod.pods.core.apple
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class BasicSingleApplePodsTest : BaseAirPodsTest() { class BasicSingleApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test mapping`() = runBlockingTest { fun `test mapping`() = runTest {
create<SingleApplePods>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") { create<SingleApplePods>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0520.toUShort() rawDeviceModel shouldBe 0x0520.toUShort()
@@ -22,7 +22,7 @@ class BasicSingleApplePodsTest : BaseAirPodsTest() {
} }
@Test @Test
fun `test battery headset percent`() = runBlockingTest { fun `test battery headset percent`() = runTest {
create<SingleApplePods>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") { create<SingleApplePods>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") {
batteryHeadsetPercent shouldBe 0.5f batteryHeadsetPercent shouldBe 0.5f
} }
@@ -1,13 +1,13 @@
package eu.darken.capod.pods.core.apple package eu.darken.capod.pods.core.apple
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class DualApplePodsTest : BaseAirPodsTest() { class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test bit mapping`() = runBlockingTest { 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<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") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
@@ -23,7 +23,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
} }
@Test @Test
fun `test AirPodDevice - active microphone`() = runBlockingTest { 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<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") {
// 00101011 // 00101011
// --^----- // --^-----
@@ -39,7 +39,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
} }
@Test @Test
fun `test AirPodDevice - left pod ear status`() = runBlockingTest { fun `test AirPodDevice - left pod ear status`() = runTest {
// Left Pod primary // Left Pod primary
create<DualAirPods>("07 19 01 0E 20 >22< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<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") {
// 00100010 // 00100010
@@ -66,7 +66,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
} }
@Test @Test
fun `test AirPodDevice - right pod ear status`() = runBlockingTest { fun `test AirPodDevice - right pod ear status`() = runTest {
// Left Pod primary // Left Pod primary
create<DualAirPods>("07 19 01 0E 20 >29< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<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") {
// 00101001 // 00101001
@@ -93,7 +93,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
} }
@Test @Test
fun `test AirPodDevice - battery status`() = runBlockingTest { fun `test AirPodDevice - battery status`() = runTest {
// Right Pod is primary // Right Pod is primary
create<DualAirPods>("07 19 01 0E 20 0B >98< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") { create<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") {
// 88 10001000 // 88 10001000
@@ -109,7 +109,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
} }
@Test @Test
fun `test AirPodDevice - pod charging`() = runBlockingTest { fun `test AirPodDevice - pod charging`() = runTest {
/** /**
* Right pod is charging * Right pod is charging
*/ */
@@ -157,7 +157,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
} }
@Test @Test
fun `test AirPodDevice - case charging`() = runBlockingTest { 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<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") {
// 0011 0011 // 0011 0011
isCaseCharging shouldBe false isCaseCharging shouldBe false
@@ -169,7 +169,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
} }
@Test @Test
fun `test AirPodDevice - case lid test`() = runBlockingTest { fun `test AirPodDevice - case lid test`() = runTest {
// Lid open // Lid open
create<DualAirPods>("07 19 01 0E 20 55 AA B4 >31< 00 00 A1 D0 BD 82 D3 52 86 CA FC 11 62 DC 42 C6 92 8E") { create<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") {
// 31 0011 0001 // 31 0011 0001
@@ -210,7 +210,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
} }
@Test @Test
fun `test AirPodDevice - connection state`() = runBlockingTest { fun `test AirPodDevice - connection state`() = runTest {
// Disconnected // Disconnected
create<DualAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >00< 62 D4 BB F1 A7 F8 64 98 D2 C8 BD 7B 3A EF 2E 15") { create<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") {
// 31 0011 0001 // 31 0011 0001
@@ -2,13 +2,13 @@ package eu.darken.capod.pods.core.apple
import eu.darken.capod.pods.core.apple.airpods.AirPodsMax import eu.darken.capod.pods.core.apple.airpods.AirPodsMax
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class SingleApplePodsTest : BaseAirPodsTest() { class SingleApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `default bit mapping Max`() = runBlockingTest { fun `default bit mapping Max`() = runTest {
create<SingleApplePods>("07 19 01 0A 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") { create<SingleApplePods>("07 19 01 0A 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0A20.toUShort() rawDeviceModel shouldBe 0x0A20.toUShort()
@@ -23,7 +23,7 @@ class SingleApplePodsTest : BaseAirPodsTest() {
} }
@Test @Test
fun `test values based on AirPodMax`() = runBlockingTest { fun `test values based on AirPodMax`() = runTest {
create<AirPodsMax>("07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E") { create<AirPodsMax>("07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E") {
batteryHeadsetPercent shouldBe 0.5f batteryHeadsetPercent shouldBe 0.5f
@@ -4,14 +4,14 @@ import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.HasAppleColor import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class AirPodsGen1Test : BaseAirPodsTest() { class AirPodsGen1Test : BaseAirPodsTest() {
// Test data from https://github.com/adolfintel/OpenPods/issues/39#issuecomment-557664269 // Test data from https://github.com/adolfintel/OpenPods/issues/39#issuecomment-557664269
@Test @Test
fun `fake airpods`() = runBlockingTest { fun `fake airpods`() = runTest {
create<AirPodsGen1>("07 19 01 02 20 55 AF 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") { create<AirPodsGen1>("07 19 01 02 20 55 AF 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0220.toUShort() rawDeviceModel shouldBe 0x0220.toUShort()
@@ -4,13 +4,13 @@ import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.HasAppleColor import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class AirPodsGen2Test : BaseAirPodsTest() { class AirPodsGen2Test : BaseAirPodsTest() {
@Test @Test
fun `random Neighbor AirPodsGen2`() = runBlockingTest { fun `random Neighbor AirPodsGen2`() = runTest {
create<AirPodsGen2>("07 19 01 0F 20 02 F9 8F 01 00 05 F2 7E 14 E0 54 0A 53 69 5B 7D F2 15 1F D7 B1 12") { create<AirPodsGen2>("07 19 01 0F 20 02 F9 8F 01 00 05 F2 7E 14 E0 54 0A 53 69 5B 7D F2 15 1F D7 B1 12") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0F20.toUShort() rawDeviceModel shouldBe 0x0F20.toUShort()
@@ -4,13 +4,13 @@ import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualAirPods import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.HasAppleColor import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class AirPodsGen3Test : BaseAirPodsTest() { class AirPodsGen3Test : BaseAirPodsTest() {
@Test @Test
fun `AirPods Gen3`() = runBlockingTest { fun `AirPods Gen3`() = runTest {
create<AirPodsGen3>("07 19 01 13 20 75 AA B9 31 00 04 67 9A 57 DF BE F6 90 52 B0 04 1F 8D 89 DA F4 9E") { create<AirPodsGen3>("07 19 01 13 20 75 AA B9 31 00 04 67 9A 57 DF BE F6 90 52 B0 04 1F 8D 89 DA F4 9E") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x1320.toUShort() rawDeviceModel shouldBe 0x1320.toUShort()
@@ -42,7 +42,7 @@ class AirPodsGen3Test : BaseAirPodsTest() {
} }
@Test @Test
fun `random guy at bus stop`() = runBlockingTest { fun `random guy at bus stop`() = runTest {
create<AirPodsGen3>("07 19 01 13 20 2B 88 8F 01 00 08 E2 0E 84 37 C5 98 16 D4 B7 37 ED 23 8B 08 EA A1") { create<AirPodsGen3>("07 19 01 13 20 2B 88 8F 01 00 08 E2 0E 84 37 C5 98 16 D4 B7 37 ED 23 8B 08 EA A1") {
batteryLeftPodPercent shouldBe 0.8f batteryLeftPodPercent shouldBe 0.8f
batteryRightPodPercent shouldBe 0.8f batteryRightPodPercent shouldBe 0.8f
@@ -2,14 +2,14 @@ package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class AirPodsMaxTest : BaseAirPodsTest() { class AirPodsMaxTest : BaseAirPodsTest() {
// Test data from https://github.com/adolfintel/OpenPods/issues/124 // Test data from https://github.com/adolfintel/OpenPods/issues/124
@Test @Test
fun `default AirPods Max`() = runBlockingTest { fun `default AirPods Max`() = runTest {
create<AirPodsMax>("07 19 01 0A 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") { create<AirPodsMax>("07 19 01 0A 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0A20.toUShort() rawDeviceModel shouldBe 0x0A20.toUShort()
@@ -31,7 +31,7 @@ class AirPodsMaxTest : BaseAirPodsTest() {
// Test data from https://github.com/adolfintel/OpenPods/issues/124 // Test data from https://github.com/adolfintel/OpenPods/issues/124
@Test @Test
fun `default AirPods Max flipped values`() = runBlockingTest { fun `default AirPods Max flipped values`() = runTest {
create<AirPodsMax>("07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E") { create<AirPodsMax>("07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0A20.toUShort() rawDeviceModel shouldBe 0x0A20.toUShort()
@@ -3,7 +3,7 @@ package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.HasAppleColor import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class AirPodsPro2Test : BaseAirPodsTest() { class AirPodsPro2Test : BaseAirPodsTest() {
@@ -12,7 +12,7 @@ class AirPodsPro2Test : BaseAirPodsTest() {
* https://github.com/d4rken-org/capod/issues/31#issuecomment-1256791084 * https://github.com/d4rken-org/capod/issues/31#issuecomment-1256791084
*/ */
@Test @Test
fun `test AirPods Pro 2 - unknown setup from #31`() = runBlockingTest { fun `test AirPods Pro 2 - unknown setup from #31`() = runTest {
create<AirPodsPro2>("07 19 01 14 20 55 88 F9 51 00 04 20 50 03 CA D5 C9 AC 0F FA 84 78 94 5A 4D DF F5") { create<AirPodsPro2>("07 19 01 14 20 55 88 F9 51 00 04 20 50 03 CA D5 C9 AC 0F FA 84 78 94 5A 4D DF F5") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
@@ -47,7 +47,7 @@ class AirPodsPro2Test : BaseAirPodsTest() {
* https://old.reddit.com/message/messages/1hst12h * https://old.reddit.com/message/messages/1hst12h
*/ */
@Test @Test
fun `test AirPods Pro 2 - unknown setup from reddit user`() = runBlockingTest { fun `test AirPods Pro 2 - unknown setup from reddit user`() = runTest {
create<AirPodsPro2>("07 19 01 14 20 2B 9A 8F 01 00 04 0F 26 1A C4 2B FA 2F B9 B6 08 CD 60 CB DF 75 AB") { create<AirPodsPro2>("07 19 01 14 20 2B 9A 8F 01 00 04 0F 26 1A C4 2B FA 2F B9 B6 08 CD 60 CB DF 75 AB") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
@@ -3,13 +3,13 @@ package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.HasAppleColor import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class AirPodsProTest : BaseAirPodsTest() { class AirPodsProTest : BaseAirPodsTest() {
@Test @Test
fun `test AirPods Pro - default changed and in case`() = runBlockingTest { fun `test AirPods Pro - default changed and in case`() = runTest {
create<AirPodsPro>("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<AirPodsPro>("07 19 01 0E 20 54 AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
@@ -38,7 +38,7 @@ class AirPodsProTest : BaseAirPodsTest() {
} }
@Test @Test
fun `test AirPods from my downstairs neighbour`() = runBlockingTest { fun `test AirPods from my downstairs neighbour`() = runTest {
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") { 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") {
isLeftPodMicrophone shouldBe false isLeftPodMicrophone shouldBe false
isRightPodMicrophone shouldBe true isRightPodMicrophone shouldBe true
@@ -55,7 +55,7 @@ class AirPodsProTest : BaseAirPodsTest() {
// Test data from https://github.com/adolfintel/OpenPods/issues/34#issuecomment-565894487 // Test data from https://github.com/adolfintel/OpenPods/issues/34#issuecomment-565894487
@Test @Test
fun `various AirPods Pro messages`() = runBlockingTest { fun `various AirPods Pro messages`() = runTest {
create<AirPodsPro>("0719010e202b668f01000500000000000000000000000000000000") { create<AirPodsPro>("0719010e202b668f01000500000000000000000000000000000000") {
batteryLeftPodPercent shouldBe 0.6f batteryLeftPodPercent shouldBe 0.6f
batteryRightPodPercent shouldBe 0.6f batteryRightPodPercent shouldBe 0.6f
@@ -159,7 +159,7 @@ class AirPodsProTest : BaseAirPodsTest() {
// Test data from https://github.com/adolfintel/OpenPods/issues/39#issuecomment-557664269 // Test data from https://github.com/adolfintel/OpenPods/issues/39#issuecomment-557664269
@Test @Test
fun `fake airpods`() = runBlockingTest { fun `fake airpods`() = runTest {
create<AirPodsGen1>("071901022055AA563100006FE4DF10AF106081033B76D9C7112288") { create<AirPodsGen1>("071901022055AA563100006FE4DF10AF106081033B76D9C7112288") {
batteryLeftPodPercent shouldBe 1.0f batteryLeftPodPercent shouldBe 1.0f
batteryRightPodPercent shouldBe 1.0f batteryRightPodPercent shouldBe 1.0f
@@ -170,4 +170,19 @@ class AirPodsProTest : BaseAirPodsTest() {
batteryCasePercent shouldBe 0.6f batteryCasePercent shouldBe 0.6f
} }
} }
@Test
fun `left pod has no data`() = runTest {
create<AirPodsPro>("07 19 01 0E 20 0B F9 8F 03 00 05 5B 59 67 4C F7 F3 EF 01 BA F4 92 1B 26 E4 90 40") {
rawPodsBattery shouldBe 0xF9.toUByte()
batteryLeftPodPercent shouldBe null
batteryRightPodPercent shouldBe 0.9f
isCaseCharging shouldBe false
isRightPodCharging shouldBe false
isLeftPodCharging shouldBe false
batteryCasePercent shouldBe null
}
}
} }
@@ -2,14 +2,14 @@ package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class BeatsFlexText : BaseAirPodsTest() { class BeatsFlexText : BaseAirPodsTest() {
// Raw data from https://github.com/adolfintel/OpenPods/issues/105 // Raw data from https://github.com/adolfintel/OpenPods/issues/105
@Test @Test
fun `default BeatsFlex`() = runBlockingTest { fun `default BeatsFlex`() = runTest {
create<BeatsFlex>("07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0") { create<BeatsFlex>("07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x1020.toUShort() rawDeviceModel shouldBe 0x1020.toUShort()
@@ -26,7 +26,7 @@ class BeatsFlexText : BaseAirPodsTest() {
} }
@Test @Test
fun `random neighbour`() = runBlockingTest { fun `random neighbour`() = runTest {
create<BeatsFlex>("07 19 01 10 20 0A F6 8F 02 4F 00 95 68 94 9E 99 D6 90 F4 5E 68 3C 58 21 68 9F 0D") { create<BeatsFlex>("07 19 01 10 20 0A F6 8F 02 4F 00 95 68 94 9E 99 D6 90 F4 5E 68 3C 58 21 68 9F 0D") {
batteryHeadsetPercent shouldBe 0.6f batteryHeadsetPercent shouldBe 0.6f
@@ -2,14 +2,14 @@ package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class BeatsSolo3Test : BaseAirPodsTest() { class BeatsSolo3Test : BaseAirPodsTest() {
// TODO This is handcrafted data, get actual data for tests // TODO This is handcrafted data, get actual data for tests
@Test @Test
fun `default BeatsSolo3`() = runBlockingTest { fun `default BeatsSolo3`() = runTest {
create<BeatsSolo3>("07 19 01 06 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") { create<BeatsSolo3>("07 19 01 06 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0620.toUShort() rawDeviceModel shouldBe 0x0620.toUShort()
@@ -2,14 +2,14 @@ package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class BeatsStudio3Test : BaseAirPodsTest() { class BeatsStudio3Test : BaseAirPodsTest() {
// TODO This is handcrafted data, get actual data for tests // TODO This is handcrafted data, get actual data for tests
@Test @Test
fun `default BeatsStudio3`() = runBlockingTest { fun `default BeatsStudio3`() = runTest {
create<BeatsStudio3>("07 19 01 09 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") { create<BeatsStudio3>("07 19 01 09 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0920.toUShort() rawDeviceModel shouldBe 0x0920.toUShort()
@@ -2,14 +2,14 @@ package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class BeatsXTest : BaseAirPodsTest() { class BeatsXTest : BaseAirPodsTest() {
// Raw data from https://github.com/adolfintel/OpenPods/issues/105 // Raw data from https://github.com/adolfintel/OpenPods/issues/105
@Test @Test
fun `default BeatsX`() = runBlockingTest { fun `default BeatsX`() = runTest {
create<BeatsX>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") { create<BeatsX>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0520.toUShort() rawDeviceModel shouldBe 0x0520.toUShort()
@@ -2,14 +2,14 @@ package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class PowerBeats3Test : BaseAirPodsTest() { class PowerBeats3Test : BaseAirPodsTest() {
// TODO This is handcrafted data, get actual data for tests // TODO This is handcrafted data, get actual data for tests
@Test @Test
fun `default PowerBeats3`() = runBlockingTest { fun `default PowerBeats3`() = runTest {
create<PowerBeats3>("07 19 01 03 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") { create<PowerBeats3>("07 19 01 03 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0320.toUShort() rawDeviceModel shouldBe 0x0320.toUShort()
@@ -3,14 +3,14 @@ package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.HasAppleColor import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class PowerBeatsProTest : BaseAirPodsTest() { class PowerBeatsProTest : BaseAirPodsTest() {
// TODO This is handcrafted data, get actual data for tests // TODO This is handcrafted data, get actual data for tests
@Test @Test
fun `test PowerBeatsPro`() = runBlockingTest { fun `test PowerBeatsPro`() = runTest {
create<PowerBeatsPro>("07 19 01 0B 20 54 AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<PowerBeatsPro>("07 19 01 0B 20 54 AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0B20.toUShort() rawDeviceModel shouldBe 0x0B20.toUShort()
@@ -2,13 +2,13 @@ package eu.darken.capod.pods.core.apple.misc
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class Twsi99999Test : BaseAirPodsTest() { class Twsi99999Test : BaseAirPodsTest() {
@Test @Test
fun `charging in box`() = runBlockingTest { fun `charging in box`() = runTest {
create<Twsi99999>("07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00") { create<Twsi99999>("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() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0220.toUShort() rawDeviceModel shouldBe 0x0220.toUShort()
@@ -2,13 +2,13 @@ package eu.darken.capod.pods.core.apple.misc
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class VarunrAirPodsProTest : BaseAirPodsTest() { class VarunrAirPodsProTest : BaseAirPodsTest() {
@Test @Test
fun `guessed data`() = runBlockingTest { fun `guessed data`() = runTest {
create<VarunrAirPodsPro>("07 13 01 0E 20 71 AA 37 36 00 10 00 FF 64 FF 00 00 00 00 00 00") { create<VarunrAirPodsPro>("07 13 01 0E 20 71 AA 37 36 00 10 00 FF 64 FF 00 00 00 00 00 00") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0E20.toUShort() rawDeviceModel shouldBe 0x0E20.toUShort()
-1
View File
@@ -38,7 +38,6 @@
android:name=".App" android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
@@ -130,24 +130,25 @@ class OverviewFragmentVM @Inject constructor(
if (permissions.isEmpty() && isBluetoothEnabled) { if (permissions.isEmpty() && isBluetoothEnabled) {
pods.map { pods.map {
val now = Instant.now() val now = Instant.now()
val isMainPod = it.identifier == mainPod?.identifier
when (it) { when (it) {
is DualPodDevice -> DualPodsCardVH.Item( is DualPodDevice -> DualPodsCardVH.Item(
now = now, now = now,
device = it, device = it,
showDebug = isDebugMode, showDebug = isDebugMode,
isMainPod = it == mainPod, isMainPod = isMainPod,
) )
is SinglePodDevice -> SinglePodsCardVH.Item( is SinglePodDevice -> SinglePodsCardVH.Item(
now = now, now = now,
device = it, device = it,
showDebug = isDebugMode, showDebug = isDebugMode,
isMainPod = it == mainPod, isMainPod = isMainPod,
) )
else -> UnknownPodDeviceCardVH.Item( else -> UnknownPodDeviceCardVH.Item(
now = now, now = now,
device = it, device = it,
showDebug = isDebugMode, showDebug = isDebugMode,
isMainPod = it == mainPod, isMainPod = isMainPod,
) )
} }
}.run { items.addAll(this) } }.run { items.addAll(this) }
@@ -0,0 +1,5 @@
package eu.darken.capod.main.ui.settings.general
sealed class GeneralSettingsEvents {
object SelectDeviceAddressEvent : GeneralSettingsEvents()
}
@@ -61,6 +61,13 @@ class GeneralSettingsFragment : PreferenceFragment3() {
true true
} }
} }
vm.events.observe2 {
when (it) {
GeneralSettingsEvents.SelectDeviceAddressEvent -> mainDeviceAddressPref.performClick()
}
}
mainDeviceModelPref.setOnPreferenceClickListener { mainDeviceModelPref.setOnPreferenceClickListener {
val dialog = ModelSelectionDialogFactory(requireContext()).create( val dialog = ModelSelectionDialogFactory(requireContext()).create(
PodDevice.Model.values().toList(), PodDevice.Model.values().toList(),
@@ -5,21 +5,41 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.withPrevious
import eu.darken.capod.common.livedata.SingleLiveEvent
import eu.darken.capod.common.uix.ViewModel3 import eu.darken.capod.common.uix.ViewModel3
import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.main.core.MonitorMode
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class GeneralSettingsFragmentVM @Inject constructor( class GeneralSettingsFragmentVM @Inject constructor(
private val handle: SavedStateHandle, @Suppress("unused") private val handle: SavedStateHandle,
private val dispatcherProvider: DispatcherProvider, dispatcherProvider: DispatcherProvider,
private val bluetoothManager: BluetoothManager2, bluetoothManager: BluetoothManager2,
private val generalSettings: GeneralSettings,
) : ViewModel3(dispatcherProvider) { ) : ViewModel3(dispatcherProvider) {
val bondedDevices = bluetoothManager.bondedDevices() val bondedDevices = bluetoothManager.bondedDevices()
.map { it.toList() } .map { it.toList() }
.asLiveData2() .asLiveData2()
val events = SingleLiveEvent<GeneralSettingsEvents>()
init {
generalSettings.monitorMode.flow
.withPrevious()
.filter { (old, new) ->
old != new && new == MonitorMode.AUTOMATIC && generalSettings.mainDeviceAddress.value == null
}
.onEach { events.postValue(GeneralSettingsEvents.SelectDeviceAddressEvent) }
.launchInViewModel()
}
companion object { companion object {
private val TAG = logTag("Settings", "General", "VM") private val TAG = logTag("Settings", "General", "VM")
} }
@@ -1,6 +1,7 @@
package eu.darken.capod.monitor.core.worker package eu.darken.capod.monitor.core.worker
import android.app.NotificationManager import android.app.NotificationManager
import android.bluetooth.BluetoothDevice
import android.content.Context import android.content.Context
import androidx.hilt.work.HiltWorker import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
@@ -114,21 +115,27 @@ class MonitorWorker @AssistedInject constructor(
} }
.launchIn(workerScope) .launchIn(workerScope)
generalSettings.monitorMode.flow permissionTool.missingPermissions
.flatMapLatest { monitorMode -> .flatMapLatest { missingPermsFlow ->
val missingPermsFlow = permissionTool.missingPermissions.first()
if (missingPermsFlow.isNotEmpty()) { if (missingPermsFlow.isNotEmpty()) {
log(TAG, WARN) { "Aborting, permissions are missing for $monitorMode: $missingPermsFlow" } log(TAG, WARN) { "Aborting, permissions are missing: $missingPermsFlow" }
workerScope.coroutineContext.cancelChildren() workerScope.coroutineContext.cancelChildren()
return@flatMapLatest emptyFlow() return@flatMapLatest emptyFlow()
} }
combine(
bluetoothManager.connectedDevices().map { knownDevices -> generalSettings.monitorMode.flow,
monitorMode to knownDevices generalSettings.mainDeviceAddress.flow,
bluetoothManager.connectedDevices(),
) { monitorMode, mainAddress, connectedDevices ->
listOf(monitorMode, mainAddress, connectedDevices)
} }
} }
.setupCommonEventHandlers(TAG) { "MonitorMode" } .setupCommonEventHandlers(TAG) { "MonitorMode" }
.flatMapLatest { (monitorMode, devices) -> .flatMapLatest { arguments ->
val monitorMode = arguments[0] as MonitorMode
val mainAddress = arguments[1] as String?
val devices = arguments[2] as Collection<BluetoothDevice>
log(TAG) { "Monitor mode: $monitorMode" } log(TAG) { "Monitor mode: $monitorMode" }
when (monitorMode) { when (monitorMode) {
MonitorMode.MANUAL -> flow<Unit> { MonitorMode.MANUAL -> flow<Unit> {
@@ -137,15 +144,20 @@ class MonitorWorker @AssistedInject constructor(
} }
MonitorMode.ALWAYS -> emptyFlow() MonitorMode.ALWAYS -> emptyFlow()
MonitorMode.AUTOMATIC -> flow { MonitorMode.AUTOMATIC -> flow {
val mainAddress = generalSettings.mainDeviceAddress.value when {
if (devices.any { it.address == mainAddress }) { mainAddress == null && devices.isNotEmpty() -> {
log(TAG) { "MainDevice is connected ($mainAddress), aborting any timeout." } log(TAG, WARN) { "Main device address not set, staying alive while any is connected" }
} else { }
log(TAG) { "No Pods are connected, canceling worker soon." } devices.any { it.address == mainAddress } -> {
delay(15 * 1000) log(TAG) { "MainDevice is connected ($mainAddress), aborting any timeout." }
log(TAG) { "Canceling worker now, still no Pods connected." } }
else -> {
log(TAG) { "No known Pods are connected, canceling worker soon." }
delay(15 * 1000)
log(TAG) { "Canceling worker now, still no Pods connected." }
workerScope.coroutineContext.cancelChildren() workerScope.coroutineContext.cancelChildren()
}
} }
} }
} }
@@ -0,0 +1,40 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="28.575"
android:viewportHeight="28.575">
<path
android:pathData="M14.288,4.762A9.525,9.525 0,0 0,7.327 7.787A6.91,6.91 0,0 0,14.101 13.357A6.91,6.91 0,0 0,20.934 7.472A9.525,9.525 0,0 0,14.288 4.762zM11.876,7.789A1.08,1.08 0,0 1,12.956 8.869A1.08,1.08 0,0 1,11.876 9.949A1.08,1.08 0,0 1,10.797 8.869A1.08,1.08 0,0 1,11.876 7.789zM16.301,7.789A1.08,1.08 0,0 1,17.381 8.869A1.08,1.08 0,0 1,16.301 9.949A1.08,1.08 0,0 1,15.221 8.869A1.08,1.08 0,0 1,16.301 7.789z"
android:strokeLineJoin="round"
android:strokeWidth="0.147417"
android:fillColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M11.33,12.714L10.689,13.825A1.038,1.038 75,0 1,9.271 14.205L9.271,14.205A1.038,1.038 75,0 1,8.891 12.787L9.533,11.676A1.038,1.038 75,0 1,10.95 11.296L10.95,11.296A1.038,1.038 75,0 1,11.33 12.714z"
android:strokeLineJoin="round"
android:strokeWidth="0.119"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M16.833,12.714L17.475,13.825A1.038,1.038 105,0 0,18.893 14.205L18.893,14.205A1.038,1.038 105,0 0,19.273 12.787L18.631,11.676A1.038,1.038 105,0 0,17.213 11.296L17.213,11.296A1.038,1.038 105,0 0,16.833 12.714z"
android:strokeLineJoin="round"
android:strokeWidth="0.119"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M11.331,15.805C10.562,15.802 9.752,16.161 9.018,16.825L9.018,19.202C9.921,20.377 10.92,20.352 11.931,20.192L11.931,23.51A9.525,9.525 0,0 0,13.607 23.787L13.607,17.279C13.018,16.272 12.202,15.807 11.331,15.805zM10.253,17.712A0.306,0.306 0,0 1,10.559 18.017A0.306,0.306 0,0 1,10.253 18.323A0.306,0.306 0,0 1,9.948 18.017A0.306,0.306 0,0 1,10.253 17.712z"
android:strokeLineJoin="round"
android:strokeWidth="0.10993"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M16.915,15.805C16.044,15.807 15.228,16.272 14.638,17.279L14.638,23.805A9.525,9.525 0,0 0,16.315 23.593L16.315,20.192C17.326,20.352 18.325,20.377 19.228,19.202L19.228,16.825C18.494,16.161 17.684,15.802 16.915,15.805zM18.031,17.712A0.306,0.306 0,0 1,18.337 18.017A0.306,0.306 0,0 1,18.031 18.323A0.306,0.306 0,0 1,17.725 18.017A0.306,0.306 0,0 1,18.031 17.712z"
android:strokeLineJoin="round"
android:strokeWidth="0.10993"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
</vector>
@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background" /> <background android:drawable="@mipmap/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon> </adaptive-icon>
+1 -1
View File
@@ -1,6 +1,6 @@
### Updated by release.sh ### ### Updated by release.sh ###
project.versioning.major=2 project.versioning.major=2
project.versioning.minor=3 project.versioning.minor=3
project.versioning.patch=0 project.versioning.patch=1
project.versioning.build=0 project.versioning.build=0
############################# #############################