mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
19
Commits
v2.2.1-rc1
...
v2.3.1-rc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07be4b8258 | ||
|
|
217087c6d4 | ||
|
|
2315a18c86 | ||
|
|
f59e9d2dc5 | ||
|
|
1da220085c | ||
|
|
962b2a01f1 | ||
|
|
eeda14f424 | ||
|
|
6269e20085 | ||
|
|
3d05296eb2 | ||
|
|
e90ab95cd0 | ||
|
|
23ec2f025e | ||
|
|
624898dd5e | ||
|
|
c6d6604399 | ||
|
|
7473704954 | ||
|
|
7a6b216bf6 | ||
|
|
1ef09124af | ||
|
|
6340f73d61 | ||
|
|
b9f4992a33 | ||
|
|
07a1fee013 |
@@ -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://play.google.com/store/apps/details?id=eu.darken.capod) |
|
||||||
|
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | ? |
|
||||||
|
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [](https://github.com/d4rken-org/capod/releases/latest) |
|
||||||
|
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [](https://apt.izzysoft.de/packages/eu.darken.capod/) |
|
||||||
|
|
||||||
|
|
||||||
## Support the project
|
## Support the project
|
||||||
|
|
||||||
@@ -54,7 +59,7 @@ Currently supported models:
|
|||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
<img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/1.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/2.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/3.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/4.png" width="200">
|
<img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/1.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/2.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/3.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/4.png" width="200">
|
||||||
|
<img src="https://raw.githubusercontent.com/d4rken-org/capod/main/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" width="200">
|
||||||
## Thanks to
|
## Thanks to
|
||||||
|
|
||||||
* The [OpenPods](https://github.com/adolfintel/OpenPods) project,
|
* The [OpenPods](https://github.com/adolfintel/OpenPods) project,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import eu.darken.capod.common.debug.logging.log
|
|||||||
import eu.darken.capod.common.error.hasCause
|
import eu.darken.capod.common.error.hasCause
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@@ -71,4 +72,11 @@ fun <T> Flow<T>.setupCommonEventHandlers(tag: String, identifier: () -> String)
|
|||||||
log(tag, ERROR) { "${identifier()} failed: ${it.asLog()}" }
|
log(tag, ERROR) { "${identifier()} failed: ${it.asLog()}" }
|
||||||
throw it
|
throw it
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Flow<T>.throttleLatest(delayMillis: Long): Flow<T> = this
|
||||||
|
.conflate()
|
||||||
|
.transform {
|
||||||
|
emit(it)
|
||||||
|
delay(delayMillis)
|
||||||
}
|
}
|
||||||
@@ -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(
|
||||||
@@ -176,6 +178,15 @@ class PodMonitor @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun latestMainDevice(): PodDevice? {
|
||||||
|
val currentMain = mainDevice.firstOrNull()
|
||||||
|
log(TAG) { "Live mainDevice is $currentMain" }
|
||||||
|
|
||||||
|
return currentMain ?: podDeviceCache.loadMainDevice()
|
||||||
|
?.let { podFactory.createPod(it)?.device }
|
||||||
|
.also { log(TAG) { "Cached mainDevice is $it" } }
|
||||||
|
}
|
||||||
|
|
||||||
private fun getUnfilteredFilter(): ScanFilter {
|
private fun getUnfilteredFilter(): ScanFilter {
|
||||||
return ScanFilter.Builder().build()
|
return ScanFilter.Builder().build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||||
|
<string name="app_name">CAPod</string>
|
||||||
|
<string name="app_name_pro">CAPod Pro</string>
|
||||||
|
<string name="app_name_foss">CAPod FOSS</string>
|
||||||
<string name="general_value_not_available_label">T/A</string>
|
<string name="general_value_not_available_label">T/A</string>
|
||||||
|
<string name="general_error_label">Error</string>
|
||||||
<string name="general_grant_permission_action">Berikan izin</string>
|
<string name="general_grant_permission_action">Berikan izin</string>
|
||||||
<string name="overview_nomaindevice_label">Tidak ada perangkat utama</string>
|
<string name="overview_nomaindevice_label">Tidak ada perangkat utama</string>
|
||||||
<string name="overview_nomaindevice_description">Semua perangkat yang terdeteksi tidak mungkin menjadi milik anda. Nyalakan & sambungkan perangkat anda atau sesuaikan pengaturan.</string>
|
<string name="overview_nomaindevice_description">Semua perangkat yang terdeteksi tidak mungkin menjadi milik anda. Nyalakan & sambungkan perangkat anda atau sesuaikan pengaturan.</string>
|
||||||
@@ -10,6 +14,7 @@
|
|||||||
<string name="permission_bluetooth_connect_description">Aplikasi ini memerlukan izin \"Koneksi Bluetooth\" untuk berinteraksi dengan perangkat yang akan dipasangkan & memulai koneksi.</string>
|
<string name="permission_bluetooth_connect_description">Aplikasi ini memerlukan izin \"Koneksi Bluetooth\" untuk berinteraksi dengan perangkat yang akan dipasangkan & memulai koneksi.</string>
|
||||||
<string name="permission_bluetooth_scan_label">Pemindaian Bluetooth</string>
|
<string name="permission_bluetooth_scan_label">Pemindaian Bluetooth</string>
|
||||||
<string name="permission_bluetooth_scan_description">Izin \"Pemindaian Bluetooth\" memungkinkan aplikasi ini menemukan & menerima data Bluetooth dari perangkat terdekat seperti AirPods anda.</string>
|
<string name="permission_bluetooth_scan_description">Izin \"Pemindaian Bluetooth\" memungkinkan aplikasi ini menemukan & menerima data Bluetooth dari perangkat terdekat seperti AirPods anda.</string>
|
||||||
|
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||||
<string name="permission_bluetooth_description">Aplikasi ini memerlukan izin \"Bluetooth\" untuk terhubung ke perangkat bluetooth yang ingin dipasangkan.</string>
|
<string name="permission_bluetooth_description">Aplikasi ini memerlukan izin \"Bluetooth\" untuk terhubung ke perangkat bluetooth yang ingin dipasangkan.</string>
|
||||||
<string name="permission_access_fine_location_label">Akses lokasi halus</string>
|
<string name="permission_access_fine_location_label">Akses lokasi halus</string>
|
||||||
<string name="permission_access_fine_location_description">CAPod menggunakan izin \"lokasi halus\" untuk menerima data Bluetooth Hemat Energi. Headphone anda menggunakan teknologi Bluetooth Hemat Energi untuk menyiarkan statusnya. Aplikasi ini TIDAK akan menggunakan data Bluetooth untuk menentukan lokasi anda.</string>
|
<string name="permission_access_fine_location_description">CAPod menggunakan izin \"lokasi halus\" untuk menerima data Bluetooth Hemat Energi. Headphone anda menggunakan teknologi Bluetooth Hemat Energi untuk menyiarkan statusnya. Aplikasi ini TIDAK akan menggunakan data Bluetooth untuk menentukan lokasi anda.</string>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||||
|
<string name="general_value_not_available_label">T/A</string>
|
||||||
<string name="general_error_label">Ralat</string>
|
<string name="general_error_label">Ralat</string>
|
||||||
<string name="general_grant_permission_action">Berikan kebenaran</string>
|
<string name="general_grant_permission_action">Berikan kebenaran</string>
|
||||||
<string name="overview_nomaindevice_label">Tiada peranti utama</string>
|
<string name="overview_nomaindevice_label">Tiada peranti utama</string>
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
<string name="permission_bluetooth_connect_description">Apl ini memerlukan kebenaran \"Bluetooth connect\" untuk berinteraksi dengan pasangan peranti dan memulakan sambungan.</string>
|
<string name="permission_bluetooth_connect_description">Apl ini memerlukan kebenaran \"Bluetooth connect\" untuk berinteraksi dengan pasangan peranti dan memulakan sambungan.</string>
|
||||||
<string name="permission_bluetooth_scan_label">Pengimbasan bluetooth</string>
|
<string name="permission_bluetooth_scan_label">Pengimbasan bluetooth</string>
|
||||||
<string name="permission_bluetooth_scan_description">Kebenaran \"Pengimbasan Bluetooth\" membolehkan apl ini menemui dan menerima data Bluetooth daripada peranti berdekatan seperti AirPods anda.</string>
|
<string name="permission_bluetooth_scan_description">Kebenaran \"Pengimbasan Bluetooth\" membolehkan apl ini menemui dan menerima data Bluetooth daripada peranti berdekatan seperti AirPods anda.</string>
|
||||||
|
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||||
<string name="permission_bluetooth_description">Apl ini memerlukan kebenaran \"Bluetooth\" untuk menyambung ke peranti bluetooth yang dipasangkan.</string>
|
<string name="permission_bluetooth_description">Apl ini memerlukan kebenaran \"Bluetooth\" untuk menyambung ke peranti bluetooth yang dipasangkan.</string>
|
||||||
<string name="permission_access_fine_location_label">Akses lokasi yang baik</string>
|
<string name="permission_access_fine_location_label">Akses lokasi yang baik</string>
|
||||||
<string name="permission_access_fine_location_description">CAPod menggunakan kebenaran \"lokasi baik\" untuk menerima data Bluetooth Tenaga Rendah. Fon kepala anda menggunakan teknologi Bluetooth Tenaga Rendah untuk menyiarkan statusnya. Apl ini TIDAK akan menggunakan data Bluetooth untuk menentukan lokasi anda.</string>
|
<string name="permission_access_fine_location_description">CAPod menggunakan kebenaran \"lokasi baik\" untuk menerima data Bluetooth Tenaga Rendah. Fon kepala anda menggunakan teknologi Bluetooth Tenaga Rendah untuk menyiarkan statusnya. Apl ini TIDAK akan menggunakan data Bluetooth untuk menentukan lokasi anda.</string>
|
||||||
|
|||||||
+4
-4
@@ -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>()
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -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
-2
@@ -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()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import eu.darken.capod.common.debug.autoreport.DebugSettings
|
|||||||
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.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.flow.combine
|
import eu.darken.capod.common.flow.combine
|
||||||
|
import eu.darken.capod.common.flow.throttleLatest
|
||||||
import eu.darken.capod.common.livedata.SingleLiveEvent
|
import eu.darken.capod.common.livedata.SingleLiveEvent
|
||||||
import eu.darken.capod.common.permissions.Permission
|
import eu.darken.capod.common.permissions.Permission
|
||||||
import eu.darken.capod.common.uix.ViewModel3
|
import eu.darken.capod.common.uix.ViewModel3
|
||||||
@@ -76,8 +77,8 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
permissionTool.missingPermissions,
|
permissionTool.missingPermissions,
|
||||||
debugSettings.isDebugModeEnabled.flow,
|
debugSettings.isDebugModeEnabled.flow,
|
||||||
bluetoothManager.isBluetoothEnabled,
|
bluetoothManager.isBluetoothEnabled,
|
||||||
podMonitor.mainDevice,
|
podMonitor.mainDevice.throttleLatest(1000),
|
||||||
) { _, permissions, isDebugMode, isBluetoothEnabled, mainPod ->
|
) { _, permissions, isDebugMode, isBluetoothEnabled, _ ->
|
||||||
val items = mutableListOf<OverviewAdapter.Item>()
|
val items = mutableListOf<OverviewAdapter.Item>()
|
||||||
|
|
||||||
if (permissions.isNotEmpty()) {
|
if (permissions.isNotEmpty()) {
|
||||||
@@ -97,10 +98,7 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
return@combine items
|
return@combine items
|
||||||
}
|
}
|
||||||
|
|
||||||
val podToShow = mainPod ?: podDeviceCache.loadMainDevice()?.let {
|
val podToShow = podMonitor.latestMainDevice()
|
||||||
log(TAG, VERBOSE) { "Using podDeviceCache: $it" }
|
|
||||||
podFactory.createPod(it)?.device
|
|
||||||
}
|
|
||||||
log(TAG, VERBOSE) { "Showing $podToShow" }
|
log(TAG, VERBOSE) { "Showing $podToShow" }
|
||||||
|
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
val customProguardRules = fileTree(File("../proguard")) {
|
val customProguardRules = fileTree(File(projectDir, "proguard")) {
|
||||||
include("*.pro")
|
include("*.pro")
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -69,6 +68,18 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".main.ui.widget.WidgetProvider"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/battery_widget_info" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
|
|||||||
@@ -8,8 +8,16 @@ import dagger.hilt.android.HiltAndroidApp
|
|||||||
import eu.darken.capod.common.coroutine.AppScope
|
import eu.darken.capod.common.coroutine.AppScope
|
||||||
import eu.darken.capod.common.debug.autoreport.AutoReporting
|
import eu.darken.capod.common.debug.autoreport.AutoReporting
|
||||||
import eu.darken.capod.common.debug.logging.*
|
import eu.darken.capod.common.debug.logging.*
|
||||||
|
import eu.darken.capod.common.flow.throttleLatest
|
||||||
|
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||||
|
import eu.darken.capod.main.ui.widget.WidgetManager
|
||||||
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.monitor.core.worker.MonitorControl
|
import eu.darken.capod.monitor.core.worker.MonitorControl
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -19,6 +27,9 @@ open class App : Application(), Configuration.Provider {
|
|||||||
@Inject lateinit var workerFactory: HiltWorkerFactory
|
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||||
@Inject lateinit var autoReporting: AutoReporting
|
@Inject lateinit var autoReporting: AutoReporting
|
||||||
@Inject lateinit var monitorControl: MonitorControl
|
@Inject lateinit var monitorControl: MonitorControl
|
||||||
|
@Inject lateinit var podMonitor: PodMonitor
|
||||||
|
@Inject lateinit var widgetManager: WidgetManager
|
||||||
|
@Inject lateinit var upgradeRepo: UpgradeRepo
|
||||||
@Inject @AppScope lateinit var appScope: CoroutineScope
|
@Inject @AppScope lateinit var appScope: CoroutineScope
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -36,6 +47,24 @@ open class App : Application(), Configuration.Provider {
|
|||||||
appScope.launch {
|
appScope.launch {
|
||||||
monitorControl.startMonitor(forceStart = true)
|
monitorControl.startMonitor(forceStart = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
podMonitor.mainDevice
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.throttleLatest(1000)
|
||||||
|
.onEach {
|
||||||
|
log(TAG) { "Main device changed, refreshing widgets." }
|
||||||
|
widgetManager.refreshWidgets()
|
||||||
|
}
|
||||||
|
.launchIn(appScope)
|
||||||
|
|
||||||
|
upgradeRepo.upgradeInfo
|
||||||
|
.map { it.isPro }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach {
|
||||||
|
log(TAG) { "Pro status changed, refreshing widgets." }
|
||||||
|
widgetManager.refreshWidgets()
|
||||||
|
}
|
||||||
|
.launchIn(appScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getWorkManagerConfiguration(): Configuration = Configuration.Builder()
|
override fun getWorkManagerConfiguration(): Configuration = Configuration.Builder()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import eu.darken.capod.common.coroutine.DispatcherProvider
|
|||||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.flow.combine
|
import eu.darken.capod.common.flow.combine
|
||||||
|
import eu.darken.capod.common.flow.throttleLatest
|
||||||
import eu.darken.capod.common.livedata.SingleLiveEvent
|
import eu.darken.capod.common.livedata.SingleLiveEvent
|
||||||
import eu.darken.capod.common.navigation.navVia
|
import eu.darken.capod.common.navigation.navVia
|
||||||
import eu.darken.capod.common.permissions.Permission
|
import eu.darken.capod.common.permissions.Permission
|
||||||
@@ -96,6 +97,7 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.catch { errorEvents.postValue(it) }
|
.catch { errorEvents.postValue(it) }
|
||||||
|
.throttleLatest(1000)
|
||||||
|
|
||||||
val listItems: LiveData<List<OverviewAdapter.Item>> = combine(
|
val listItems: LiveData<List<OverviewAdapter.Item>> = combine(
|
||||||
updateTicker,
|
updateTicker,
|
||||||
@@ -128,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(),
|
||||||
|
|||||||
+23
-3
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package eu.darken.capod.main.ui.widget
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class WidgetManager @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val widgetManager by lazy { AppWidgetManager.getInstance(context) }
|
||||||
|
|
||||||
|
private val currentWidgetIds: IntArray
|
||||||
|
get() = widgetManager.getAppWidgetIds(ComponentName(context, PROVIDER_CLASS))
|
||||||
|
|
||||||
|
suspend fun refreshWidgets() {
|
||||||
|
log(TAG) { "refreshWidgets()" }
|
||||||
|
|
||||||
|
log(TAG, VERBOSE) { "Notifying these widget IDs: ${currentWidgetIds.toList()}" }
|
||||||
|
|
||||||
|
val intent = Intent(context, PROVIDER_CLASS).apply {
|
||||||
|
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||||
|
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, currentWidgetIds)
|
||||||
|
}
|
||||||
|
context.sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val PROVIDER_CLASS = WidgetProvider::class.java
|
||||||
|
val TAG = logTag("Widget", "Manager")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
package eu.darken.capod.main.ui.widget
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.appwidget.AppWidgetProvider
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import eu.darken.capod.R
|
||||||
|
import eu.darken.capod.common.coroutine.AppScope
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
|
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.common.upgrade.UpgradeRepo
|
||||||
|
import eu.darken.capod.common.upgrade.isPro
|
||||||
|
import eu.darken.capod.main.ui.MainActivity
|
||||||
|
import eu.darken.capod.monitor.core.PodDeviceCache
|
||||||
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
|
import eu.darken.capod.pods.core.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import java.time.Duration
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class WidgetProvider : AppWidgetProvider() {
|
||||||
|
|
||||||
|
@Inject lateinit var podMonitor: PodMonitor
|
||||||
|
@Inject lateinit var podDeviceCache: PodDeviceCache
|
||||||
|
@Inject lateinit var podFactory: PodFactory
|
||||||
|
@Inject lateinit var upgradeRepo: UpgradeRepo
|
||||||
|
@AppScope @Inject lateinit var appScope: CoroutineScope
|
||||||
|
|
||||||
|
private var asyncBarrier: PendingResult? = null
|
||||||
|
|
||||||
|
private fun executeAsync(
|
||||||
|
tag: String,
|
||||||
|
timeout: Duration = Duration.ofSeconds(10),
|
||||||
|
block: suspend () -> Unit
|
||||||
|
) {
|
||||||
|
val start = System.currentTimeMillis()
|
||||||
|
asyncBarrier = goAsync()
|
||||||
|
log(TAG, VERBOSE) { "executeAsync($tag) starting asyncBarrier=$asyncBarrier " }
|
||||||
|
|
||||||
|
appScope.launch {
|
||||||
|
try {
|
||||||
|
withTimeout(timeout.toMillis()) { block() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(TAG, ERROR) { "executeAsync($tag) failed: ${e.asLog()}" }
|
||||||
|
} finally {
|
||||||
|
asyncBarrier?.finish()
|
||||||
|
val stop = System.currentTimeMillis()
|
||||||
|
log(TAG, VERBOSE) { "executeAsync($tag) DONE (${stop - start}ms) " }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(TAG, VERBOSE) { "executeAsync($block) leaving" }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||||
|
log(TAG) { "onUpdate(appWidgetIds=${appWidgetIds.toList()})" }
|
||||||
|
executeAsync("onUpdate") {
|
||||||
|
appWidgetIds.forEach { appWidgetId ->
|
||||||
|
updateWidget(context, appWidgetManager, appWidgetId, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAppWidgetOptionsChanged(
|
||||||
|
context: Context,
|
||||||
|
appWidgetManager: AppWidgetManager,
|
||||||
|
appWidgetId: Int,
|
||||||
|
newOptions: Bundle?
|
||||||
|
) {
|
||||||
|
log(TAG) { "onAppWidgetOptionsChanged(appWidgetId=$appWidgetId, newOptions=$newOptions)" }
|
||||||
|
executeAsync("onAppWidgetOptionsChanged") {
|
||||||
|
updateWidget(context, appWidgetManager, appWidgetId, newOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateWidget(
|
||||||
|
context: Context,
|
||||||
|
widgetManager: AppWidgetManager,
|
||||||
|
widgetId: Int,
|
||||||
|
options: Bundle?
|
||||||
|
) {
|
||||||
|
log(TAG) { "updateWidget(widgetId=$widgetId, options=$options)" }
|
||||||
|
|
||||||
|
val device: PodDevice? = podMonitor.latestMainDevice()
|
||||||
|
|
||||||
|
val layout = when {
|
||||||
|
!upgradeRepo.isPro() -> createUpgradeRequiredLayout(context)
|
||||||
|
device is DualPodDevice -> createDualPodLayout(context, device)
|
||||||
|
device is SinglePodDevice -> createSinglePodLayout(context, device)
|
||||||
|
device is PodDevice -> createUnknownPodLayout(context, device)
|
||||||
|
else -> createNoDeviceLayout(context)
|
||||||
|
}
|
||||||
|
widgetManager.updateAppWidget(widgetId, layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun createUpgradeRequiredLayout(
|
||||||
|
context: Context
|
||||||
|
) = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
|
||||||
|
log(TAG, VERBOSE) { "createUpgradeRequiredLayout(context=$context)" }
|
||||||
|
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
Intent(context, MainActivity::class.java),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||||
|
|
||||||
|
setTextViewText(R.id.primary, context.getString(R.string.upgrade_capod_label))
|
||||||
|
setTextViewText(R.id.secondary, context.getString(R.string.upgrade_capod_description))
|
||||||
|
setViewVisibility(R.id.secondary, View.VISIBLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createUnknownPodLayout(
|
||||||
|
context: Context,
|
||||||
|
podDevice: PodDevice,
|
||||||
|
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
|
||||||
|
log(TAG, VERBOSE) { "createUnknownPodLayout(context=$context, podDevice=$podDevice)" }
|
||||||
|
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
Intent(context, MainActivity::class.java),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||||
|
|
||||||
|
setTextViewText(R.id.primary, context.getString(R.string.pods_unknown_label))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNoDeviceLayout(
|
||||||
|
context: Context,
|
||||||
|
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
|
||||||
|
log(TAG, VERBOSE) { "createNoDeviceLayout(context=$context)" }
|
||||||
|
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
Intent(context, MainActivity::class.java),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||||
|
|
||||||
|
setTextViewText(R.id.primary, context.getString(R.string.overview_nomaindevice_label))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDualPodLayout(
|
||||||
|
context: Context,
|
||||||
|
podDevice: DualPodDevice,
|
||||||
|
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_pod_dual_layout).apply {
|
||||||
|
log(TAG, VERBOSE) { "createSinglePodLayout(context=$context, podDevice=$podDevice)" }
|
||||||
|
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
Intent(context, MainActivity::class.java),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||||
|
|
||||||
|
setTextViewText(R.id.headphones_label, podDevice.getLabel(context))
|
||||||
|
|
||||||
|
// Left
|
||||||
|
setTextViewText(R.id.pod_left_label, podDevice.getBatteryLevelLeftPod(context))
|
||||||
|
setViewVisibility(
|
||||||
|
R.id.pod_left_charging,
|
||||||
|
if (podDevice is HasChargeDetectionDual && podDevice.isLeftPodCharging) View.VISIBLE else View.GONE
|
||||||
|
)
|
||||||
|
setViewVisibility(
|
||||||
|
R.id.pod_left_ear,
|
||||||
|
if (podDevice is HasEarDetectionDual && podDevice.isLeftPodInEar) View.VISIBLE else View.GONE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Case
|
||||||
|
setTextViewText(R.id.pod_case_label, (podDevice as? HasCase)?.getBatteryLevelCase(context))
|
||||||
|
setViewVisibility(
|
||||||
|
R.id.pod_case_charging,
|
||||||
|
if (podDevice is HasCase && podDevice.isCaseCharging) View.VISIBLE else View.GONE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Right
|
||||||
|
setTextViewText(R.id.pod_right_label, podDevice.getBatteryLevelRightPod(context))
|
||||||
|
setViewVisibility(
|
||||||
|
R.id.pod_right_charging,
|
||||||
|
if (podDevice is HasChargeDetectionDual && podDevice.isRightPodCharging) View.VISIBLE else View.GONE
|
||||||
|
)
|
||||||
|
setViewVisibility(
|
||||||
|
R.id.pod_right_ear,
|
||||||
|
if (podDevice is HasEarDetectionDual && podDevice.isRightPodInEar) View.VISIBLE else View.GONE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSinglePodLayout(
|
||||||
|
context: Context,
|
||||||
|
podDevice: SinglePodDevice,
|
||||||
|
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_pod_single_layout).apply {
|
||||||
|
log(TAG, VERBOSE) { "createSinglePodLayout(context=$context, podDevice=$podDevice)" }
|
||||||
|
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
Intent(context, MainActivity::class.java),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||||
|
|
||||||
|
setTextViewText(R.id.headphones_label, podDevice.getLabel(context))
|
||||||
|
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(podDevice.batteryHeadsetPercent))
|
||||||
|
setTextViewText(R.id.headphones_battery_label, podDevice.getBatteryLevelHeadset(context))
|
||||||
|
|
||||||
|
setViewVisibility(
|
||||||
|
R.id.headphones_worn,
|
||||||
|
if (podDevice is HasEarDetection && podDevice.isBeingWorn) View.VISIBLE else View.GONE
|
||||||
|
)
|
||||||
|
|
||||||
|
if (this is HasChargeDetectionDual) {
|
||||||
|
setViewVisibility(R.id.headphones_charging, if (isHeadsetBeingCharged) View.VISIBLE else View.GONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = logTag("Widget", "Provider")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -17,9 +18,11 @@ 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.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
|
import eu.darken.capod.common.flow.throttleLatest
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
import eu.darken.capod.main.core.MonitorMode
|
import eu.darken.capod.main.core.MonitorMode
|
||||||
import eu.darken.capod.main.core.PermissionTool
|
import eu.darken.capod.main.core.PermissionTool
|
||||||
|
import eu.darken.capod.main.ui.widget.WidgetManager
|
||||||
import eu.darken.capod.monitor.core.MonitorComponent
|
import eu.darken.capod.monitor.core.MonitorComponent
|
||||||
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
@@ -48,6 +51,7 @@ class MonitorWorker @AssistedInject constructor(
|
|||||||
private val autoConnect: AutoConnect,
|
private val autoConnect: AutoConnect,
|
||||||
private val popUpReaction: PopUpReaction,
|
private val popUpReaction: PopUpReaction,
|
||||||
private val popUpWindow: PopUpWindow,
|
private val popUpWindow: PopUpWindow,
|
||||||
|
private val widgetManager: WidgetManager,
|
||||||
) : CoroutineWorker(context, params) {
|
) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
private val workerScope = MonitorCoroutineScope()
|
private val workerScope = MonitorCoroutineScope()
|
||||||
@@ -99,6 +103,7 @@ class MonitorWorker @AssistedInject constructor(
|
|||||||
.setupCommonEventHandlers(TAG) { "PodMonitor" }
|
.setupCommonEventHandlers(TAG) { "PodMonitor" }
|
||||||
.onStart { setForeground(monitorNotifications.getForegroundInfo(null)) }
|
.onStart { setForeground(monitorNotifications.getForegroundInfo(null)) }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
.throttleLatest(1000)
|
||||||
.onEach { currentDevice ->
|
.onEach { currentDevice ->
|
||||||
notificationManager.notify(
|
notificationManager.notify(
|
||||||
MonitorNotifications.NOTIFICATION_ID,
|
MonitorNotifications.NOTIFICATION_ID,
|
||||||
@@ -110,38 +115,49 @@ 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> {
|
||||||
// Cancel worker, ui scans manually
|
// Cancel worker, ui scans manually
|
||||||
workerScope.coroutineContext.cancelChildren()
|
workerScope.coroutineContext.cancelChildren()
|
||||||
}
|
}
|
||||||
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>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
style="@style/Notification.Container"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:layout_marginHorizontal="8dp"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
style="@style/Notification.Container"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_horizontal">
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
style="@style/Notification.Container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
style="@style/PodWidget.Container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:indeterminate="true" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/widget_root"
|
||||||
|
style="@style/PodWidget.Container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/primary"
|
||||||
|
style="@style/PodWidget.TextPrimary"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/pods_unknown_label"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/secondary"
|
||||||
|
style="@style/PodWidget.TextSecondary"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="center"
|
||||||
|
android:maxLines="4"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:text="@string/pods_unknown_label"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
style="@style/PodWidget.Container"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/headphones_label"
|
||||||
|
style="@style/PodWidget.TextPrimary"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginBottom="2dp"
|
||||||
|
android:textSize="12sp"
|
||||||
|
tools:text="AirPods Max" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_icon"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_airpod_left_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/pod_left_label"
|
||||||
|
style="@style/PodInfoItemText.Notification"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="4dp"
|
||||||
|
tools:text="100%" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_charging"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_baseline_power_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_ear"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_icon"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_airpod_right_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/pod_right_label"
|
||||||
|
style="@style/PodInfoItemText.Notification"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="4dp"
|
||||||
|
tools:text="100%" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_charging"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_baseline_power_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_ear"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_case_icon"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_airpod_case_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/pod_case_label"
|
||||||
|
style="@style/PodInfoItemText.Notification"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="4dp"
|
||||||
|
tools:text="100%" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_case_charging"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
style="@style/PodWidget.Container"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/headphones_label"
|
||||||
|
style="@style/PodWidget.TextPrimary"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
tools:text="AirPods Max" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/headphones_battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/headphones_battery_label"
|
||||||
|
style="@style/PodInfoItemText.Notification"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
tools:text="100%" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/headphones_charging"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_baseline_power_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/headphones_worn"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
style="@style/PodWidget.Container"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_icon"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_airpod_left_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/pod_left_label"
|
||||||
|
style="@style/PodInfoItemText.Notification"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="100%"
|
||||||
|
tools:ignore="HardcodedText" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_charging"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_baseline_power_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_ear"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_icon"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_airpod_right_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/pod_right_label"
|
||||||
|
style="@style/PodInfoItemText.Notification"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="100%"
|
||||||
|
tools:ignore="HardcodedText" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_charging"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_baseline_power_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_ear"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_case_icon"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_airpod_case_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/pod_case_label"
|
||||||
|
style="@style/PodInfoItemText.Notification"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="100%"
|
||||||
|
tools:ignore="HardcodedText" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_case_charging"
|
||||||
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:src="@drawable/ic_baseline_power_24"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -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>
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<string name="general_upgrade_action">Tingkatkan</string>
|
<string name="general_upgrade_action">Tingkatkan</string>
|
||||||
<string name="general_check_action">Memeriksa</string>
|
<string name="general_check_action">Memeriksa</string>
|
||||||
<string name="general_close_action">Tutup</string>
|
<string name="general_close_action">Tutup</string>
|
||||||
|
<string name="upgrade_capod_label">Perbaharui CAPod</string>
|
||||||
<string name="upgrade_capod_description">Dapatkan fitur tambahan & bantu dukung pengembang.</string>
|
<string name="upgrade_capod_description">Dapatkan fitur tambahan & bantu dukung pengembang.</string>
|
||||||
<string name="settings_monitor_mode_label">Mode monitor</string>
|
<string name="settings_monitor_mode_label">Mode monitor</string>
|
||||||
<string name="settings_monitor_mode_description">Dalam keadaan apa aplikasi ini memonitor data Bluetooth.</string>
|
<string name="settings_monitor_mode_description">Dalam keadaan apa aplikasi ini memonitor data Bluetooth.</string>
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
<string name="settings_support_description">Jika anda membutuhkan bantuan.</string>
|
<string name="settings_support_description">Jika anda membutuhkan bantuan.</string>
|
||||||
<string name="issue_tracker_label">Pelacak masalah</string>
|
<string name="issue_tracker_label">Pelacak masalah</string>
|
||||||
<string name="issue_tracker_description">Pelacak masalah publik untuk laporan bug & permintaan fitur (hanya dalam bahasa Inggris).</string>
|
<string name="issue_tracker_description">Pelacak masalah publik untuk laporan bug & permintaan fitur (hanya dalam bahasa Inggris).</string>
|
||||||
|
<string name="discord_label">Discord</string>
|
||||||
<string name="discord_description">Tempat nongkrong & bertanya-tanya.</string>
|
<string name="discord_description">Tempat nongkrong & bertanya-tanya.</string>
|
||||||
<string name="changelog_label">Catatan perubahan</string>
|
<string name="changelog_label">Catatan perubahan</string>
|
||||||
<string name="settings_label">Pengaturan</string>
|
<string name="settings_label">Pengaturan</string>
|
||||||
@@ -73,4 +75,5 @@
|
|||||||
<string name="help_translate_label">Terjemahan</string>
|
<string name="help_translate_label">Terjemahan</string>
|
||||||
<string name="help_translate_description">Bantu terjemahkan applikasi ini ke bahasa favorit anda.</string>
|
<string name="help_translate_description">Bantu terjemahkan applikasi ini ke bahasa favorit anda.</string>
|
||||||
<string name="translators_thanks_title">Penerjemah</string>
|
<string name="translators_thanks_title">Penerjemah</string>
|
||||||
|
<string name="translators_thanks_description">gelapkan</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<item name="widget_root" type="id" />
|
||||||
|
</resources>
|
||||||
@@ -89,4 +89,6 @@
|
|||||||
|
|
||||||
<string name="translators_thanks_title">Translators</string>
|
<string name="translators_thanks_title">Translators</string>
|
||||||
<string name="translators_thanks_description">darken</string>
|
<string name="translators_thanks_description">darken</string>
|
||||||
|
|
||||||
|
<string name="widget_description">A widget showing the last known device status.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -7,14 +7,26 @@
|
|||||||
<item name="android:layout_marginEnd">8dp</item>
|
<item name="android:layout_marginEnd">8dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="Notification">
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Notification.Container">
|
||||||
|
<!-- <item name="android:background">?android:attr/colorBackground</item>-->
|
||||||
|
<item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="PodInfoItemIcon.Notification" parent="TextAppearance.Compat.Notification.Title">
|
<style name="PodInfoItemIcon.Notification" parent="TextAppearance.Compat.Notification.Title">
|
||||||
<item name="android:layout_height">20dp</item>
|
<item name="android:layout_height">20dp</item>
|
||||||
<item name="android:layout_width">20dp</item>
|
<item name="android:layout_width">20dp</item>
|
||||||
|
<item name="android:tint">?android:attr/colorAccent</item>
|
||||||
|
<item name="tint">?android:attr/colorAccent</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="PodInfoItemText.Notification" parent="TextAppearance.Compat.Notification.Title">
|
<style name="PodInfoItemText.Notification" parent="TextAppearance.Compat.Notification.Title">
|
||||||
<item name="android:singleLine">true</item>
|
<item name="android:singleLine">true</item>
|
||||||
<item name="android:ellipsize">end</item>
|
<item name="android:ellipsize">end</item>
|
||||||
|
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="PodInfoItemIcon" parent="TextAppearance.MaterialComponents.Body2">
|
<style name="PodInfoItemIcon" parent="TextAppearance.MaterialComponents.Body2">
|
||||||
@@ -26,4 +38,29 @@
|
|||||||
<item name="android:singleLine">true</item>
|
<item name="android:singleLine">true</item>
|
||||||
<item name="android:ellipsize">end</item>
|
<item name="android:ellipsize">end</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="PodWidget">
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="PodWidget.Container">
|
||||||
|
<item name="android:background">?android:attr/colorBackground</item>
|
||||||
|
<item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item>
|
||||||
|
<item name="android:paddingStart">16dp</item>
|
||||||
|
<item name="android:paddingEnd">16dp</item>
|
||||||
|
<item name="android:paddingTop">8dp</item>
|
||||||
|
<item name="android:paddingBottom">8dp</item>
|
||||||
|
<item name="android:layout_width">match_parent</item>
|
||||||
|
<item name="android:layout_height">wrap_content</item>
|
||||||
|
<item name="android:id">@id/widget_root</item>
|
||||||
|
<item name="android:gravity">center</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="PodWidget.TextPrimary">
|
||||||
|
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="PodWidget.TextSecondary">
|
||||||
|
<item name="android:textColor">?android:attr/textColorSecondary</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:minWidth="80dp"
|
||||||
|
android:minHeight="40dp"
|
||||||
|
android:targetCellWidth="2"
|
||||||
|
android:targetCellHeight="1"
|
||||||
|
android:updatePeriodMillis="86400000"
|
||||||
|
android:description="@string/widget_description"
|
||||||
|
android:previewLayout="@layout/widget_preview_layout"
|
||||||
|
android:initialLayout="@layout/widget_loading_layout"
|
||||||
|
android:resizeMode="horizontal|vertical"
|
||||||
|
android:widgetCategory="home_screen"
|
||||||
|
android:widgetFeatures="reconfigurable|configuration_optional"
|
||||||
|
tools:targetApi="s" />
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
Bugfixes and performance improvements.
|
Now with more widgets!
|
||||||
¯\_(ツ)_/¯
|
But also bugfixes and performance improvements.
|
||||||
|
¯\_(ツ)_/¯
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
### Updated by release.sh ###
|
### Updated by release.sh ###
|
||||||
project.versioning.major=2
|
project.versioning.major=2
|
||||||
project.versioning.minor=2
|
project.versioning.minor=3
|
||||||
project.versioning.patch=1
|
project.versioning.patch=1
|
||||||
project.versioning.build=1
|
project.versioning.build=0
|
||||||
#############################
|
#############################
|
||||||
|
|||||||
Reference in New Issue
Block a user