Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51f9c5c7d8 | ||
|
|
15141c8528 | ||
|
|
0cdac10fd4 | ||
|
|
aaab548bfd | ||
|
|
46611b5606 | ||
|
|
fdbdb46577 | ||
|
|
e7fd81d0eb | ||
|
|
ce50bae6a0 | ||
|
|
8331cf3866 | ||
|
|
670262e53b | ||
|
|
c584f942cf | ||
|
|
e83b6bb583 | ||
|
|
cc5de668f0 | ||
|
|
653c7b8c4b | ||
|
|
4e38bcd4cc | ||
|
|
fcbeda4888 | ||
|
|
4101fddc5b | ||
|
|
86df7bea22 | ||
|
|
4db8c0c286 | ||
|
|
1d03335903 | ||
|
|
b1b541014f | ||
|
|
b40a7e435f | ||
|
|
8305fb4c5f | ||
|
|
6c678be511 | ||
|
|
2823663f09 | ||
|
|
033c2264f8 | ||
|
|
8eb6ce19f7 | ||
|
|
1cfc4e6f65 | ||
|
|
454d76e305 | ||
|
|
f3609fc3a5 | ||
|
|
16da51d095 | ||
|
|
51fb4c7a29 | ||
|
|
0b27cb55dc | ||
|
|
9e836eef69 | ||
|
|
07be4b8258 | ||
|
|
217087c6d4 | ||
|
|
2315a18c86 | ||
|
|
f59e9d2dc5 | ||
|
|
1da220085c | ||
|
|
962b2a01f1 | ||
|
|
eeda14f424 | ||
|
|
6269e20085 | ||
|
|
3d05296eb2 | ||
|
|
e90ab95cd0 | ||
|
|
23ec2f025e | ||
|
|
624898dd5e | ||
|
|
c6d6604399 | ||
|
|
7473704954 | ||
|
|
7a6b216bf6 | ||
|
|
1ef09124af | ||
|
|
6340f73d61 | ||
|
|
b9f4992a33 | ||
|
|
07a1fee013 | ||
|
|
63f37e51ad | ||
|
|
01ea08a5cb | ||
|
|
bddb20c5bb | ||
|
|
f9a97297da | ||
|
|
5fc2b56635 | ||
|
|
172953ccb3 | ||
|
|
d55300f53d | ||
|
|
c3d4500e3a | ||
|
|
cf72e28727 |
@@ -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.
|
||||||
|
|
||||||
@@ -33,12 +34,22 @@ Currently supported models:
|
|||||||
* Beats Studio 3
|
* Beats Studio 3
|
||||||
* Beats X
|
* Beats X
|
||||||
* Beats Flex
|
* Beats Flex
|
||||||
|
* Beats Fit Pro
|
||||||
|
* Fake AirPods 1. Generation 🎭
|
||||||
|
* Fake AirPods 2. Generation 🎭
|
||||||
|
* Fake AirPods 3. Generation 🎭
|
||||||
|
* Fake AirPods Pro1 🎭
|
||||||
|
* Fake AirPods Pro2 🎭
|
||||||
|
|
||||||
## 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) | [](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 +65,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,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class BleScanner @Inject constructor(
|
|||||||
log(TAG, VERBOSE) { "scan(filters=$filters, scannerMode=$scannerMode, compatMode=$compatMode)" }
|
log(TAG, VERBOSE) { "scan(filters=$filters, scannerMode=$scannerMode, compatMode=$compatMode)" }
|
||||||
if (compatMode) log(TAG, WARN) { "Using compatibilityMode!" }
|
if (compatMode) log(TAG, WARN) { "Using compatibilityMode!" }
|
||||||
|
|
||||||
val adapter = bluetoothManager.adapter
|
val adapter = bluetoothManager.adapter ?: throw IllegalStateException("Bluetooth adapter unavailable")
|
||||||
|
|
||||||
val supportsOffloadFiltering = adapter.isOffloadedFilteringSupported.also {
|
val supportsOffloadFiltering = adapter.isOffloadedFilteringSupported.also {
|
||||||
log(TAG, if (it) DEBUG else WARN) { "isOffloadedFilteringSupported=$it" }
|
log(TAG, if (it) DEBUG else WARN) { "isOffloadedFilteringSupported=$it" }
|
||||||
@@ -45,7 +45,7 @@ class BleScanner @Inject constructor(
|
|||||||
log(TAG, if (it) DEBUG else WARN) { "isOffloadedScanBatchingSupported=$it" }
|
log(TAG, if (it) DEBUG else WARN) { "isOffloadedScanBatchingSupported=$it" }
|
||||||
} && !compatMode
|
} && !compatMode
|
||||||
|
|
||||||
val scanner = bluetoothManager.scanner
|
val scanner = bluetoothManager.scanner ?: throw IllegalStateException("BLE scanner unavailable")
|
||||||
|
|
||||||
val callback = object : ScanCallback() {
|
val callback = object : ScanCallback() {
|
||||||
var lastScanAt = System.currentTimeMillis()
|
var lastScanAt = System.currentTimeMillis()
|
||||||
|
|||||||
@@ -30,12 +30,11 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
private val dispatcherProvider: DispatcherProvider,
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val adapter: BluetoothAdapter
|
val adapter: BluetoothAdapter?
|
||||||
get() = manager.adapter
|
get() = manager.adapter
|
||||||
|
|
||||||
val scanner: BluetoothLeScanner
|
val scanner: BluetoothLeScanner?
|
||||||
get() = adapter.bluetoothLeScanner
|
get() = adapter?.bluetoothLeScanner
|
||||||
?: throw IllegalStateException("Bluetooth is disabled or permissiong missing")
|
|
||||||
|
|
||||||
val isBluetoothEnabled: Flow<Boolean> = callbackFlow {
|
val isBluetoothEnabled: Flow<Boolean> = callbackFlow {
|
||||||
send(manager.adapter?.isEnabled ?: false)
|
send(manager.adapter?.isEnabled ?: false)
|
||||||
@@ -158,7 +157,9 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bondedDevices(): Set<BluetoothDevice> = adapter.bondedDevices
|
fun bondedDevices(): Flow<Set<BluetoothDevice>> = flow {
|
||||||
|
emit(adapter?.bondedDevices ?: throw IllegalStateException("Bluetooth adapter unavailable"))
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun nudgeConnection(device: BluetoothDevice): Boolean = getBluetoothProfile().map { bluetoothProfile ->
|
suspend fun nudgeConnection(device: BluetoothDevice): Boolean = getBluetoothProfile().map { bluetoothProfile ->
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -72,3 +73,10 @@ fun <T> Flow<T>.setupCommonEventHandlers(tag: String, identifier: () -> String)
|
|||||||
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package eu.darken.capod.pods.core
|
package eu.darken.capod.pods.core
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
|
|
||||||
interface DualPodDevice : PodDevice {
|
interface DualPodDevice : PodDevice {
|
||||||
|
|
||||||
enum class Pod {
|
enum class Pod {
|
||||||
@@ -10,4 +13,12 @@ interface DualPodDevice : PodDevice {
|
|||||||
val batteryLeftPodPercent: Float?
|
val batteryLeftPodPercent: Float?
|
||||||
|
|
||||||
val batteryRightPodPercent: Float?
|
val batteryRightPodPercent: Float?
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_gen1_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_gen1_right
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
package eu.darken.capod.pods.core
|
package eu.darken.capod.pods.core
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
|
|
||||||
interface HasCase {
|
interface HasCase {
|
||||||
|
|
||||||
val batteryCasePercent: Float?
|
val batteryCasePercent: Float?
|
||||||
|
|
||||||
val isCaseCharging: Boolean
|
val isCaseCharging: Boolean
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_gen1_case
|
||||||
}
|
}
|
||||||
@@ -60,31 +60,31 @@ interface PodDevice {
|
|||||||
@JsonClass(generateAdapter = false)
|
@JsonClass(generateAdapter = false)
|
||||||
enum class Model(
|
enum class Model(
|
||||||
val label: String,
|
val label: String,
|
||||||
@DrawableRes val iconRes: Int = R.drawable.ic_device_generic_earbuds,
|
@DrawableRes val iconRes: Int = R.drawable.devic_earbuds_generic_both,
|
||||||
) {
|
) {
|
||||||
@Json(name = "airpods.gen1") AIRPODS_GEN1(
|
@Json(name = "airpods.gen1") AIRPODS_GEN1(
|
||||||
label = "AirPods (Gen 1)",
|
label = "AirPods (Gen 1)",
|
||||||
iconRes = R.drawable.ic_device_airpods_gen1,
|
iconRes = R.drawable.devic_airpods_gen1_both,
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.gen2") AIRPODS_GEN2(
|
@Json(name = "airpods.gen2") AIRPODS_GEN2(
|
||||||
"AirPods (Gen 2)",
|
"AirPods (Gen 2)",
|
||||||
R.drawable.ic_device_airpods_gen2,
|
R.drawable.devic_airpods_gen2_both,
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.gen3") AIRPODS_GEN3(
|
@Json(name = "airpods.gen3") AIRPODS_GEN3(
|
||||||
"AirPods (Gen 3)",
|
"AirPods (Gen 3)",
|
||||||
R.drawable.ic_device_airpods_gen2,
|
R.drawable.devic_airpods_gen2_both,
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.pro") AIRPODS_PRO(
|
@Json(name = "airpods.pro") AIRPODS_PRO(
|
||||||
"AirPods Pro",
|
"AirPods Pro",
|
||||||
R.drawable.ic_device_airpods_gen2
|
R.drawable.devic_airpods_pro2_both
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.pro2") AIRPODS_PRO2(
|
@Json(name = "airpods.pro2") AIRPODS_PRO2(
|
||||||
"AirPods Pro 2",
|
"AirPods Pro 2",
|
||||||
R.drawable.ic_device_airpods_gen2
|
R.drawable.devic_airpods_pro2_both
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.max") AIRPODS_MAX(
|
@Json(name = "airpods.max") AIRPODS_MAX(
|
||||||
"AirPods Max",
|
"AirPods Max",
|
||||||
R.drawable.ic_device_generic_headphones
|
R.drawable.devic_headphones_generic
|
||||||
),
|
),
|
||||||
@Json(name = "beats.flex") BEATS_FLEX(
|
@Json(name = "beats.flex") BEATS_FLEX(
|
||||||
"Beats Flex"
|
"Beats Flex"
|
||||||
@@ -104,11 +104,28 @@ interface PodDevice {
|
|||||||
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
|
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
|
||||||
"Power Beats Pro"
|
"Power Beats Pro"
|
||||||
),
|
),
|
||||||
@Json(name = "fakes.tws.i99999") TWS_I99999(
|
@Json(name = "beats.fit.pro") BEATS_FIT_PRO(
|
||||||
"TWS i99999"
|
"Beats Fit Pro"
|
||||||
),
|
),
|
||||||
@Json(name = "fakes.varunr.airpodspro") VARUNR_AIRPODS_PRO(
|
@Json(name = "fakes.tws.i99999") FAKE_AIRPODS_GEN1(
|
||||||
"Fake AirPods Pro"
|
"AirPods (Gen 1)? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_gen1_both,
|
||||||
|
),
|
||||||
|
@Json(name = "fakes.generic.airpods.gen2") FAKE_AIRPODS_GEN2(
|
||||||
|
"AirPods (Gen 2)? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_gen2_both,
|
||||||
|
),
|
||||||
|
@Json(name = "fakes.generic.airpods.gen3") FAKE_AIRPODS_GEN3(
|
||||||
|
"AirPods (Gen 3)? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_gen2_both,
|
||||||
|
),
|
||||||
|
@Json(name = "fakes.varunr.airpodspro") FAKE_AIRPODS_PRO(
|
||||||
|
"AirPods Pro? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_pro2_both,
|
||||||
|
),
|
||||||
|
@Json(name = "fakes.generic.airpods.pro2") FAKE_AIRPODS_PRO2(
|
||||||
|
"AirPods Pro2? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_pro2_both,
|
||||||
),
|
),
|
||||||
@Json(name = "unknown") UNKNOWN(
|
@Json(name = "unknown") UNKNOWN(
|
||||||
"Unknown"
|
"Unknown"
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import dagger.hilt.components.SingletonComponent
|
|||||||
import dagger.multibindings.IntoSet
|
import dagger.multibindings.IntoSet
|
||||||
import eu.darken.capod.pods.core.apple.airpods.*
|
import eu.darken.capod.pods.core.apple.airpods.*
|
||||||
import eu.darken.capod.pods.core.apple.beats.*
|
import eu.darken.capod.pods.core.apple.beats.*
|
||||||
import eu.darken.capod.pods.core.apple.misc.Twsi99999
|
import eu.darken.capod.pods.core.apple.misc.*
|
||||||
import eu.darken.capod.pods.core.apple.misc.VarunrAirPodsPro
|
|
||||||
|
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
@Module
|
@Module
|
||||||
@@ -28,8 +27,11 @@ abstract class AppleFactoryModule {
|
|||||||
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
@Binds @IntoSet abstract fun beatsFitPro(factory: BeatsFitPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
|
||||||
@Binds @IntoSet abstract fun fakesTwsi999999(factory: Twsi99999.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun fakeAirPodsGen1(factory: FakeAirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet
|
@Binds @IntoSet abstract fun fakeAirPodsGen2(factory: FakeAirPodsGen2.Factory): ApplePodsFactory<out ApplePods>
|
||||||
abstract fun fakesVarunrAirPodsPro(factory: VarunrAirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun fakeAirPodsGen3(factory: FakeAirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
@Binds @IntoSet abstract fun fakeAirPodsPro(factory: FakeAirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
@Binds @IntoSet abstract fun fakeAirPodsPro2(factory: FakeAirPodsPro2.Factory): ApplePodsFactory<out ApplePods>
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,16 +79,32 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
|||||||
.mapNotNull { it.batteryCasePercent }
|
.mapNotNull { it.batteryCasePercent }
|
||||||
.lastOrNull()
|
.lastOrNull()
|
||||||
|
|
||||||
fun KnownDevice.getLatestCaseLidState(basic: DualAirPods): DualAirPods.LidState? {
|
fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? {
|
||||||
val definitive = setOf(DualAirPods.LidState.OPEN, DualAirPods.LidState.CLOSED)
|
val definitive = setOf(DualApplePods.LidState.OPEN, DualApplePods.LidState.CLOSED)
|
||||||
if (definitive.contains(basic.caseLidState)) return null
|
if (definitive.contains(basic.caseLidState)) return null
|
||||||
|
|
||||||
return history
|
return history
|
||||||
.filterIsInstance<AirPodsPro>() // TODO why is this AirPodsPro specific here?
|
.filterIsInstance<AirPodsPro>() // TODO why is this AirPodsPro specific here?
|
||||||
.lastOrNull { it.caseLidState != DualAirPods.LidState.NOT_IN_CASE }
|
.lastOrNull { it.caseLidState != DualApplePods.LidState.NOT_IN_CASE }
|
||||||
?.caseLidState
|
?.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,8 +1,5 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import eu.darken.capod.common.R
|
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
@@ -10,8 +7,8 @@ import eu.darken.capod.common.upperNibble
|
|||||||
import eu.darken.capod.pods.core.*
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.DualPodDevice.Pod
|
import eu.darken.capod.pods.core.DualPodDevice.Pod
|
||||||
|
|
||||||
interface DualAirPods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase,
|
interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase,
|
||||||
HasStateDetection, HasDualMicrophone, HasAppleColor {
|
HasDualMicrophone, HasAppleColor {
|
||||||
|
|
||||||
val primaryPod: Pod
|
val primaryPod: Pod
|
||||||
get() = when (rawStatus.isBitSet(5)) {
|
get() = when (rawStatus.isBitSet(5)) {
|
||||||
@@ -142,21 +139,4 @@ interface DualAirPods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEar
|
|||||||
UNKNOWN(0xFF..0xFF);
|
UNKNOWN(0xFF..0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
override val state: ConnectionState
|
|
||||||
get() = ConnectionState.values().firstOrNull { rawSuffix == it.raw } ?: ConnectionState.UNKNOWN
|
|
||||||
|
|
||||||
enum class ConnectionState(val raw: UByte?, @StringRes val labelRes: Int) : HasStateDetection.State {
|
|
||||||
DISCONNECTED(0x00, R.string.pods_connection_state_disconnected_label),
|
|
||||||
IDLE(0x04, R.string.pods_connection_state_idle_label),
|
|
||||||
MUSIC(0x05, R.string.pods_connection_state_music_label),
|
|
||||||
CALL(0x06, R.string.pods_connection_state_call_label),
|
|
||||||
RINGING(0x07, R.string.pods_connection_state_ringing_label),
|
|
||||||
HANGING_UP(0x09, R.string.pods_connection_state_hanging_up_label),
|
|
||||||
UNKNOWN(null, R.string.pods_connection_state_unknown_label);
|
|
||||||
|
|
||||||
override fun getLabel(context: Context): String = context.getString(labelRes)
|
|
||||||
|
|
||||||
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,9 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
|
|||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
|
||||||
abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualAirPods>(tag) {
|
abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualApplePods>(tag) {
|
||||||
|
|
||||||
fun DualAirPods.getCaseMatchMarkings() = SplitPodsMarkings(
|
fun DualApplePods.getCaseMatchMarkings() = SplitPodsMarkings(
|
||||||
leftPodBattery = batteryLeftPodPercent,
|
leftPodBattery = batteryLeftPodPercent,
|
||||||
rightPodBattery = batteryRightPodPercent,
|
rightPodBattery = batteryRightPodPercent,
|
||||||
microPhoneLeft = isLeftPodMicrophone,
|
microPhoneLeft = isLeftPodMicrophone,
|
||||||
@@ -31,17 +31,17 @@ abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<
|
|||||||
val model: PodDevice.Model,
|
val model: PodDevice.Model,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualAirPods): Collection<KnownDevice> {
|
private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualApplePods): Collection<KnownDevice> {
|
||||||
val target = device.getCaseMatchMarkings()
|
val target = device.getCaseMatchMarkings()
|
||||||
|
|
||||||
return filter { known ->
|
return filter { known ->
|
||||||
known.history
|
known.history
|
||||||
.filterIsInstance<DualAirPods>()
|
.filterIsInstance<DualApplePods>()
|
||||||
.any { it.getCaseMatchMarkings() == target }
|
.any { it.getCaseMatchMarkings() == target }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchHistory(current: DualAirPods): KnownDevice? {
|
override fun searchHistory(current: DualApplePods): KnownDevice? {
|
||||||
val basicResult = super.searchHistory(current)
|
val basicResult = super.searchHistory(current)
|
||||||
|
|
||||||
val caseIgnored = knownDevices.values.findSplitPodsMatch(current)
|
val caseIgnored = knownDevices.values.findSplitPodsMatch(current)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -20,15 +20,15 @@ data class AirPodsGen1 constructor(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualAirPods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualAirPods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
override val caseLidState: DualAirPods.LidState
|
override val caseLidState: DualApplePods.LidState
|
||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -20,19 +20,19 @@ data class AirPodsGen2 constructor(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualAirPods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualAirPods {
|
) : DualApplePods, HasStateDetectionAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
override val caseLidState: DualAirPods.LidState
|
override val caseLidState: DualApplePods.LidState
|
||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -20,18 +20,19 @@ data class AirPodsGen3 constructor(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualAirPods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualAirPods {
|
) : DualApplePods, HasStateDetectionAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
override val caseLidState: DualAirPods.LidState
|
override val caseLidState: DualApplePods.LidState
|
||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ data class AirPodsMax(
|
|||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods, HasEarDetection {
|
) : SingleApplePods, HasEarDetection, HasStateDetectionAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super<SingleApplePods>.rssi
|
||||||
|
|
||||||
val isHeadphonesBeingWorn: Boolean
|
val isHeadphonesBeingWorn: Boolean
|
||||||
get() = rawStatus.isBitSet(1)
|
get() = rawStatus.isBitSet(1)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package eu.darken.capod.pods.core.apple.airpods
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
|
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -22,10 +24,26 @@ data class AirPodsPro(
|
|||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: LidState? = null
|
private val cachedCaseState: LidState? = null
|
||||||
) : DualAirPods {
|
) : DualApplePods, HasStateDetectionAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val iconRes: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_both
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_case
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_right
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
@@ -33,7 +51,7 @@ data class AirPodsPro(
|
|||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package eu.darken.capod.pods.core.apple.airpods
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
|
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -22,10 +24,26 @@ data class AirPodsPro2(
|
|||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: LidState? = null
|
private val cachedCaseState: LidState? = null
|
||||||
) : DualAirPods {
|
) : DualApplePods, HasStateDetectionAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO2
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO2
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val iconRes: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_both
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_case
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_right
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
@@ -33,7 +51,7 @@ data class AirPodsPro2(
|
|||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super<HasStateDetectionAirPods>.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
|
import eu.darken.capod.pods.core.HasStateDetection
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
|
|
||||||
|
interface HasStateDetectionAirPods : HasStateDetection, ApplePods {
|
||||||
|
|
||||||
|
override val state: ConnectionState
|
||||||
|
get() = ConnectionState.values().firstOrNull { rawSuffix == it.raw } ?: ConnectionState.UNKNOWN
|
||||||
|
|
||||||
|
enum class ConnectionState(val raw: UByte?, @StringRes val labelRes: Int) : HasStateDetection.State {
|
||||||
|
DISCONNECTED(0x00, R.string.pods_connection_state_disconnected_label),
|
||||||
|
IDLE(0x04, R.string.pods_connection_state_idle_label),
|
||||||
|
MUSIC(0x05, R.string.pods_connection_state_music_label),
|
||||||
|
CALL(0x06, R.string.pods_connection_state_call_label),
|
||||||
|
RINGING(0x07, R.string.pods_connection_state_ringing_label),
|
||||||
|
HANGING_UP(0x09, R.string.pods_connection_state_hanging_up_label),
|
||||||
|
UNKNOWN(null, R.string.pods_connection_state_unknown_label);
|
||||||
|
|
||||||
|
override fun getLabel(context: Context): String = context.getString(labelRes)
|
||||||
|
|
||||||
|
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.beats
|
||||||
|
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
data class BeatsFitPro(
|
||||||
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
|
override val scanResult: BleScanResult,
|
||||||
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
|
) : DualApplePods {
|
||||||
|
|
||||||
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_FIT_PRO
|
||||||
|
|
||||||
|
override val batteryCasePercent: Float?
|
||||||
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
|
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
|
var basic = BeatsFitPro(scanResult = scanResult, proximityMessage = message)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
|
updateHistory(basic)
|
||||||
|
|
||||||
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenLastAt = scanResult.receivedAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x1220.toUShort()
|
||||||
|
private val TAG = logTag("PodDevice", "Beats", "Fit", "Pro")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -20,15 +20,15 @@ data class PowerBeatsPro(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualAirPods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualAirPods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
override val caseLidState: DualAirPods.LidState
|
override val caseLidState: DualApplePods.LidState
|
||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.pods.core.DualPodDevice
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.HasCase
|
|
||||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
@@ -19,8 +16,9 @@ import javax.inject.Inject
|
|||||||
/**
|
/**
|
||||||
* Basically an AirPods GEN1 clone
|
* Basically an AirPods GEN1 clone
|
||||||
* Similar data structure but a lot of placeholder values or hardcoded values
|
* Similar data structure but a lot of placeholder values or hardcoded values
|
||||||
|
* Marketed as TWS i99999
|
||||||
*/
|
*/
|
||||||
data class Twsi99999 constructor(
|
data class FakeAirPodsGen1 constructor(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
@@ -30,9 +28,9 @@ data class Twsi99999 constructor(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
|
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.TWS_I99999
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN1
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super<ApplePods>.rssi
|
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||||
@@ -78,22 +76,32 @@ data class Twsi99999 constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isThisPodInThecase: Boolean
|
private val isThisPodInThecase: Boolean
|
||||||
get() = rawStatus.isBitSet(6)
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
/**
|
override val isLeftPodInEar: Boolean
|
||||||
* The data flip bit is set if the left pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(3)
|
||||||
*/
|
false -> rawStatus.isBitSet(1)
|
||||||
override val isLeftPodMicrophone: Boolean
|
}
|
||||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
|
||||||
/**
|
override val isRightPodInEar: Boolean
|
||||||
* The data flip bit is UNset if the right pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(1)
|
||||||
*/
|
false -> rawStatus.isBitSet(3)
|
||||||
override val isRightPodMicrophone: Boolean
|
}
|
||||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
override val isLeftPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(0)
|
||||||
|
true -> rawFlags.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(1)
|
||||||
|
true -> rawFlags.isBitSet(0)
|
||||||
|
}
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = when (val value = rawCaseBattery.toInt()) {
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
@@ -109,7 +117,7 @@ data class Twsi99999 constructor(
|
|||||||
override val isCaseCharging: Boolean
|
override val isCaseCharging: Boolean
|
||||||
get() = rawFlags.isBitSet(2)
|
get() = rawFlags.isBitSet(2)
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePodsFactory<Twsi99999>(TAG) {
|
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsGen1>(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||||
@@ -117,7 +125,7 @@ data class Twsi99999 constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
var basic = Twsi99999(scanResult = scanResult, proximityMessage = message)
|
var basic = FakeAirPodsGen1(scanResult = scanResult, proximityMessage = message)
|
||||||
val result = searchHistory(basic)
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
@@ -139,6 +147,6 @@ data class Twsi99999 constructor(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val DEVICE_CODE = 0x0220.toUShort()
|
private val DEVICE_CODE = 0x0220.toUShort()
|
||||||
private val TAG = logTag("PodDevice", "Apple", "TWS", "i99999")
|
private val TAG = logTag("PodDevice", "Apple", "Fake", "AirPods", "Gen1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,10 +6,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.pods.core.DualPodDevice
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.HasCase
|
|
||||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
@@ -17,10 +14,9 @@ import java.time.Instant
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AirPods Pro clone similar to Twsi999999.
|
* Similar data structure but a lot of placeholder values or hardcoded values
|
||||||
* Shorter data structure.
|
|
||||||
*/
|
*/
|
||||||
data class VarunrAirPodsPro constructor(
|
data class FakeAirPodsGen2 constructor(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
@@ -30,9 +26,9 @@ data class VarunrAirPodsPro constructor(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
|
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.VARUNR_AIRPODS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN2
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super<ApplePods>.rssi
|
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||||
@@ -78,22 +74,32 @@ data class VarunrAirPodsPro constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isThisPodInThecase: Boolean
|
private val isThisPodInThecase: Boolean
|
||||||
get() = rawStatus.isBitSet(6)
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
/**
|
override val isLeftPodInEar: Boolean
|
||||||
* The data flip bit is set if the left pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(3)
|
||||||
*/
|
false -> rawStatus.isBitSet(1)
|
||||||
override val isLeftPodMicrophone: Boolean
|
}
|
||||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
|
||||||
/**
|
override val isRightPodInEar: Boolean
|
||||||
* The data flip bit is UNset if the right pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(1)
|
||||||
*/
|
false -> rawStatus.isBitSet(3)
|
||||||
override val isRightPodMicrophone: Boolean
|
}
|
||||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
override val isLeftPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(0)
|
||||||
|
true -> rawFlags.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(1)
|
||||||
|
true -> rawFlags.isBitSet(0)
|
||||||
|
}
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = when (val value = rawCaseBattery.toInt()) {
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
@@ -109,7 +115,7 @@ data class VarunrAirPodsPro constructor(
|
|||||||
override val isCaseCharging: Boolean
|
override val isCaseCharging: Boolean
|
||||||
get() = rawFlags.isBitSet(2)
|
get() = rawFlags.isBitSet(2)
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePodsFactory<VarunrAirPodsPro>(TAG) {
|
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsGen2>(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||||
@@ -117,7 +123,7 @@ data class VarunrAirPodsPro constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
var basic = VarunrAirPodsPro(scanResult = scanResult, proximityMessage = message)
|
var basic = FakeAirPodsGen2(scanResult = scanResult, proximityMessage = message)
|
||||||
val result = searchHistory(basic)
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
@@ -138,7 +144,7 @@ data class VarunrAirPodsPro constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val DEVICE_CODE = 0x0E20.toUShort()
|
private val DEVICE_CODE = 0x0F20.toUShort()
|
||||||
private val TAG = logTag("PodDevice", "Apple", "Varunr", "AirPodsPro")
|
private val TAG = logTag("PodDevice", "Apple", "Fake", "AirPods", "Gen2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.common.isBitSet
|
||||||
|
import eu.darken.capod.common.lowerNibble
|
||||||
|
import eu.darken.capod.common.upperNibble
|
||||||
|
import eu.darken.capod.pods.core.*
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AirPods Gen3 clone
|
||||||
|
* Shorter data structure.
|
||||||
|
* https://discord.com/channels/548521543039189022/927235844127993866/1054404630118924308
|
||||||
|
*/
|
||||||
|
data class FakeAirPodsGen3 constructor(
|
||||||
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
|
override val scanResult: BleScanResult,
|
||||||
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||||
|
|
||||||
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN3
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
|
||||||
|
* If the right pod is the primary, the values are flipped.
|
||||||
|
*/
|
||||||
|
val areValuesFlipped: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5)
|
||||||
|
|
||||||
|
override val batteryLeftPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
false -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Left pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
(value / 10f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryRightPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
false -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Right pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isLeftPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(0)
|
||||||
|
true -> rawFlags.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(1)
|
||||||
|
true -> rawFlags.isBitSet(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isThisPodInThecase: Boolean
|
||||||
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
|
override val isLeftPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(3)
|
||||||
|
false -> rawStatus.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(1)
|
||||||
|
false -> rawStatus.isBitSet(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryCasePercent: Float?
|
||||||
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
|
15 -> cachedBatteryPercentage
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Case: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isCaseCharging: Boolean
|
||||||
|
get() = rawFlags.isBitSet(2)
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsGen3>(TAG) {
|
||||||
|
|
||||||
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
|
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||||
|
getModelInfo().full == DEVICE_CODE && length == 19
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
|
var basic = FakeAirPodsGen3(scanResult = scanResult, proximityMessage = message)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
|
updateHistory(basic)
|
||||||
|
|
||||||
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenLastAt = scanResult.receivedAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x1320.toUShort()
|
||||||
|
private val TAG = logTag("PodDevice", "Apple", "Fake", "AirPods", "Gen3")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.common.isBitSet
|
||||||
|
import eu.darken.capod.common.lowerNibble
|
||||||
|
import eu.darken.capod.common.upperNibble
|
||||||
|
import eu.darken.capod.pods.core.*
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AirPods Pro clone similar to Twsi999999.
|
||||||
|
* Shorter data structure.
|
||||||
|
* https://discord.com/channels/548521543039189022/927235844127993866/962068944196358234
|
||||||
|
*/
|
||||||
|
data class FakeAirPodsPro constructor(
|
||||||
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
|
override val scanResult: BleScanResult,
|
||||||
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
) : ApplePods, DualPodDevice, HasChargeDetectionDual, HasEarDetectionDual, HasCase {
|
||||||
|
|
||||||
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val iconRes: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_both
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_case
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_right
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
|
||||||
|
* If the right pod is the primary, the values are flipped.
|
||||||
|
*/
|
||||||
|
val areValuesFlipped: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5)
|
||||||
|
|
||||||
|
override val batteryLeftPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
false -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Left pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
(value / 10f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryRightPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
false -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Right pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isLeftPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(0)
|
||||||
|
true -> rawFlags.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(1)
|
||||||
|
true -> rawFlags.isBitSet(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isThisPodInThecase: Boolean
|
||||||
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
|
override val isLeftPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(3)
|
||||||
|
false -> rawStatus.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(1)
|
||||||
|
false -> rawStatus.isBitSet(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryCasePercent: Float?
|
||||||
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
|
15 -> cachedBatteryPercentage
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Case: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isCaseCharging: Boolean
|
||||||
|
get() = rawFlags.isBitSet(2)
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsPro>(TAG) {
|
||||||
|
|
||||||
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
|
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||||
|
getModelInfo().full == DEVICE_CODE && length == 19
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
|
var basic = FakeAirPodsPro(scanResult = scanResult, proximityMessage = message)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
|
updateHistory(basic)
|
||||||
|
|
||||||
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenLastAt = scanResult.receivedAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x0E20.toUShort()
|
||||||
|
private val TAG = logTag("PodDevice", "Apple", "Fake", "AirPods", "Pro")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.common.isBitSet
|
||||||
|
import eu.darken.capod.common.lowerNibble
|
||||||
|
import eu.darken.capod.common.upperNibble
|
||||||
|
import eu.darken.capod.pods.core.*
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AirPods Pro2 clone
|
||||||
|
* https://discord.com/channels/548521543039189022/927235844127993866/1060911213539774504
|
||||||
|
*/
|
||||||
|
data class FakeAirPodsPro2 constructor(
|
||||||
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
|
override val scanResult: BleScanResult,
|
||||||
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null,
|
||||||
|
) : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase {
|
||||||
|
|
||||||
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO2
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val iconRes: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_both
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_case
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_right
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: scanResult.rssi
|
||||||
|
|
||||||
|
private val areValuesFlipped: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5)
|
||||||
|
|
||||||
|
override val batteryLeftPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
false -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Left pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
(value / 10f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryRightPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
false -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Right pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isThisPodInThecase: Boolean
|
||||||
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
|
override val isLeftPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(3)
|
||||||
|
false -> rawStatus.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(1)
|
||||||
|
false -> rawStatus.isBitSet(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isLeftPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(0)
|
||||||
|
true -> rawFlags.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(1)
|
||||||
|
true -> rawFlags.isBitSet(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryCasePercent: Float?
|
||||||
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
|
15 -> cachedBatteryPercentage
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Case: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isCaseCharging: Boolean
|
||||||
|
get() = rawFlags.isBitSet(2)
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsPro2>(TAG) {
|
||||||
|
|
||||||
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
|
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||||
|
getModelInfo().full == DEVICE_CODE && length == 19
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
|
var basic = FakeAirPodsPro2(scanResult = scanResult, proximityMessage = message)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
|
updateHistory(basic)
|
||||||
|
|
||||||
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenLastAt = scanResult.receivedAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x1420.toUShort()
|
||||||
|
private val TAG = logTag("PodDevice", "Apple", "Fake", "AirPods", "Pro2")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import eu.darken.capod.main.core.GeneralSettings
|
|||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.reaction.core.ReactionSettings
|
import eu.darken.capod.reaction.core.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -46,7 +46,8 @@ class AutoConnect @Inject constructor(
|
|||||||
return@map
|
return@map
|
||||||
}
|
}
|
||||||
|
|
||||||
val bondedDevice = bluetoothManager.bondedDevices().firstOrNull { it.address == mainDeviceAddr }
|
val bondedDevice = bluetoothManager.bondedDevices().first().firstOrNull { it.address == mainDeviceAddr }
|
||||||
|
|
||||||
if (bondedDevice == null) {
|
if (bondedDevice == null) {
|
||||||
log(TAG, WARN) { "No bonded device matches $mainDeviceAddr" }
|
log(TAG, WARN) { "No bonded device matches $mainDeviceAddr" }
|
||||||
return@map
|
return@map
|
||||||
@@ -68,7 +69,7 @@ class AutoConnect @Inject constructor(
|
|||||||
val conditionFulfilled = when (condition) {
|
val conditionFulfilled = when (condition) {
|
||||||
AutoConnectCondition.WHEN_SEEN -> true
|
AutoConnectCondition.WHEN_SEEN -> true
|
||||||
AutoConnectCondition.CASE_OPEN -> when (mainDevice) {
|
AutoConnectCondition.CASE_OPEN -> when (mainDevice) {
|
||||||
is DualAirPods -> mainDevice.caseLidState == DualAirPods.LidState.OPEN
|
is DualApplePods -> mainDevice.caseLidState == DualApplePods.LidState.OPEN
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
AutoConnectCondition.IN_EAR -> when (mainDevice) {
|
AutoConnectCondition.IN_EAR -> when (mainDevice) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package eu.darken.capod.reaction.core.playpause
|
|||||||
import eu.darken.capod.common.MediaControl
|
import eu.darken.capod.common.MediaControl
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
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.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
|
||||||
@@ -40,46 +41,51 @@ class PlayPause @Inject constructor(
|
|||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.withPrevious()
|
.withPrevious()
|
||||||
|
.filter { (previous, current) ->
|
||||||
|
if (previous == null || current == null) return@filter false
|
||||||
|
log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" }
|
||||||
|
val match = previous.identifier == current.identifier
|
||||||
|
if (!match) log(TAG, WARN) { "Main device switched, skipping reaction." }
|
||||||
|
match
|
||||||
|
}
|
||||||
.onEach { (previous, current) ->
|
.onEach { (previous, current) ->
|
||||||
if (previous is HasEarDetection && current is HasEarDetection) {
|
log(TAG, VERBOSE) { "Checking\nprevious=$previous\ncurrent=$current" }
|
||||||
log(TAG, VERBOSE) { "previous=${previous.isBeingWorn}, current=${current.isBeingWorn}" }
|
|
||||||
log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" }
|
|
||||||
if (previous.identifier != current.identifier) {
|
|
||||||
return@onEach
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previous is HasEarDetectionDual && current is HasEarDetectionDual && reactionSettings.onePodMode.value) {
|
val previousWorn: Boolean?
|
||||||
log(TAG) { "Ear status changed for dual pod device in single pod mode." }
|
val currentWorn: Boolean?
|
||||||
val previousWorn = previous.isEitherPodInEar
|
|
||||||
val currentWorn = current.isEitherPodInEar
|
when {
|
||||||
if (!previousWorn && currentWorn && !mediaControl.isPlaying) {
|
reactionSettings.onePodMode.value && previous is HasEarDetectionDual && current is HasEarDetectionDual -> {
|
||||||
if (reactionSettings.autoPlay.value) {
|
previousWorn = previous.isEitherPodInEar
|
||||||
mediaControl.sendPlay()
|
currentWorn = current.isEitherPodInEar
|
||||||
} else {
|
log(TAG, VERBOSE) { "previous: left=${previous.isLeftPodInEar}, right=${previous.isRightPodInEar}" }
|
||||||
log(TAG) { "autoPlay is disabled" }
|
log(TAG, VERBOSE) { "current: left${current.isLeftPodInEar}, right=${current.isRightPodInEar}" }
|
||||||
}
|
}
|
||||||
} else if (!currentWorn && previousWorn && mediaControl.isPlaying) {
|
previous is HasEarDetection && current is HasEarDetection -> {
|
||||||
if (reactionSettings.autoPause.value) {
|
previousWorn = previous.isBeingWorn
|
||||||
mediaControl.sendPause()
|
currentWorn = current.isBeingWorn
|
||||||
} else {
|
log(TAG, VERBOSE) { "prev.isBeingWorn=${previousWorn}, cur.isBeingWorn=${currentWorn}" }
|
||||||
log(TAG) { "autoPause is disabled" }
|
}
|
||||||
}
|
else -> {
|
||||||
}
|
log(TAG, VERBOSE) { "Current devices don't support ear detection." }
|
||||||
} else if (previous.isBeingWorn != current.isBeingWorn) {
|
previousWorn = null
|
||||||
log(TAG) { "Ear status changed for monitored device." }
|
currentWorn = null
|
||||||
if (current.isBeingWorn && !mediaControl.isPlaying) {
|
}
|
||||||
if (reactionSettings.autoPlay.value) {
|
}
|
||||||
mediaControl.sendPlay()
|
|
||||||
} else {
|
if (previousWorn == false && currentWorn == true && !mediaControl.isPlaying) {
|
||||||
log(TAG) { "autoPlay is disabled" }
|
if (reactionSettings.autoPlay.value) {
|
||||||
}
|
log(TAG) { "autoPlay is triggered, sendPlay()" }
|
||||||
} else if (!current.isBeingWorn && mediaControl.isPlaying) {
|
mediaControl.sendPlay()
|
||||||
if (reactionSettings.autoPause.value) {
|
} else {
|
||||||
mediaControl.sendPause()
|
log(TAG, VERBOSE) { "autoPlay is disabled" }
|
||||||
} else {
|
}
|
||||||
log(TAG) { "autoPause is disabled" }
|
} else if (previousWorn == true && currentWorn == false && mediaControl.isPlaying) {
|
||||||
}
|
if (reactionSettings.autoPause.value) {
|
||||||
}
|
log(TAG) { "autoPause is triggered, sendPause()" }
|
||||||
|
mediaControl.sendPause()
|
||||||
|
} else {
|
||||||
|
log(TAG) { "autoPause is disabled" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import eu.darken.capod.common.flow.setupCommonEventHandlers
|
|||||||
import eu.darken.capod.common.flow.withPrevious
|
import eu.darken.capod.common.flow.withPrevious
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.reaction.core.ReactionSettings
|
import eu.darken.capod.reaction.core.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -34,7 +34,7 @@ class PopUpReaction @Inject constructor(
|
|||||||
.withPrevious()
|
.withPrevious()
|
||||||
.setupCommonEventHandlers(TAG) { "monitor" }
|
.setupCommonEventHandlers(TAG) { "monitor" }
|
||||||
.mapNotNull { (previous, current) ->
|
.mapNotNull { (previous, current) ->
|
||||||
if (previous !is DualAirPods? || current !is DualAirPods) {
|
if (previous !is DualApplePods? || current !is DualApplePods) {
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
log(TAG, VERBOSE) {
|
log(TAG, VERBOSE) {
|
||||||
@@ -57,8 +57,8 @@ class PopUpReaction @Inject constructor(
|
|||||||
tryPopWindow(current)
|
tryPopWindow(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun tryPopWindow(current: DualAirPods): Event? = when {
|
private suspend fun tryPopWindow(current: DualApplePods): Event? = when {
|
||||||
current.caseLidState == DualAirPods.LidState.OPEN -> {
|
current.caseLidState == DualApplePods.LidState.OPEN -> {
|
||||||
log(TAG, INFO) { "Show popup" }
|
log(TAG, INFO) { "Show popup" }
|
||||||
|
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
@@ -74,9 +74,9 @@ class PopUpReaction @Inject constructor(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
current.caseLidState != DualAirPods.LidState.OPEN -> {
|
current.caseLidState != DualApplePods.LidState.OPEN -> {
|
||||||
when (current.caseLidState) {
|
when (current.caseLidState) {
|
||||||
DualAirPods.LidState.CLOSED -> {
|
DualApplePods.LidState.CLOSED -> {
|
||||||
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
||||||
coolDowns.remove(current.identifier)
|
coolDowns.remove(current.identifier)
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -1,10 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="?attr/colorControlNormal"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="m6,11c-1.9954,0 -3,-2.0093 -3,-4s1.0046,-4 3,-4c3.1837,0 5,3.4637 5,6.4886v10.0114c0,0.8284 -0.6716,1.5 -1.5,1.5 -0.8284,0 -1.5,-0.6716 -1.5,-1.5v-8.0113c0,-0.381 -0.1426,-0.7378 -0.3906,-1.0098 -0.4267,0.3435 -0.9676,0.5211 -1.6094,0.5211zM9,11.4887v8.0113c0,0.2761 0.2239,0.5 0.5,0.5s0.5,-0.2239 0.5,-0.5v-10.0114c0,-2.5454 -1.5434,-5.4886 -4,-5.4886 -1.2546,0 -2,1.4907 -2,3s0.7454,3 2,3c0.7809,0 1.2592,-0.3588 1.5257,-1.1581 0.2094,-0.6282 1.1516,-0.3212 0.9508,0.3097l-0.1878,0.5902c0.4499,0.4603 0.7114,1.0823 0.7114,1.7468zM6,8c-0.5523,0 -1,-0.4477 -1,-1s0.4477,-1 1,-1 1,0.4477 1,1 -0.4477,1 -1,1zM16,11.4887v8.0113c0,0.8284 -0.6716,1.5 -1.5,1.5s-1.5,-0.6716 -1.5,-1.5v-10.0114c0,-3.0249 1.8163,-6.4886 5,-6.4886 1.9954,0 3,2.0093 3,4s-1.0046,4 -3,4c-0.6418,0 -1.1826,-0.1777 -1.6094,-0.5211 -0.248,0.2721 -0.3906,0.6288 -0.3906,1.0098zM15.5235,9.1516c-0.2008,-0.631 0.7414,-0.9379 0.9508,-0.3097 0.2665,0.7994 0.7448,1.1581 1.5257,1.1581 1.2546,0 2,-1.4907 2,-3s-0.7454,-3 -2,-3c-2.4566,0 -4,2.9433 -4,5.4886v10.0114c0,0.2761 0.2239,0.5 0.5,0.5s0.5,-0.2239 0.5,-0.5v-8.0113c0,-0.6645 0.2615,-1.2865 0.7114,-1.7468zM18,8c-0.5523,0 -1,-0.4477 -1,-1s0.4477,-1 1,-1 1,0.4477 1,1 -0.4477,1 -1,1z" />
|
|
||||||
</vector>
|
|
||||||
@@ -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="app_name_pro">CAPod Pro</string>
|
||||||
<string name="general_value_not_available_label">Δε διατίθεται</string>
|
<string name="general_value_not_available_label">Δε διατίθεται</string>
|
||||||
<string name="general_error_label">Σφάλμα</string>
|
<string name="general_error_label">Σφάλμα</string>
|
||||||
<string name="general_grant_permission_action">Παραχώρηση άδειας</string>
|
<string name="general_grant_permission_action">Παραχώρηση άδειας</string>
|
||||||
|
|||||||
@@ -1,2 +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>
|
<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">N/D</string>
|
||||||
|
<string name="general_error_label">Error</string>
|
||||||
|
<string name="general_grant_permission_action">Conceder permiso</string>
|
||||||
|
<string name="overview_nomaindevice_label">Ningún dispositivo principal</string>
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<?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 (libre)</string>
|
||||||
<string name="general_value_not_available_label">Non disponible</string>
|
<string name="general_value_not_available_label">Non disponible</string>
|
||||||
<string name="general_error_label">Erreur</string>
|
<string name="general_error_label">Erreur</string>
|
||||||
<string name="general_grant_permission_action">Accorder l’autorisation</string>
|
<string name="general_grant_permission_action">Accorder l’autorisation</string>
|
||||||
@@ -11,6 +14,7 @@
|
|||||||
<string name="permission_bluetooth_connect_description">Cette appli exige l’autorisation « Connexion Bluetooth » pour interagir avec les appareils jumelés et démarrer les connexions.</string>
|
<string name="permission_bluetooth_connect_description">Cette appli exige l’autorisation « Connexion Bluetooth » pour interagir avec les appareils jumelés et démarrer les connexions.</string>
|
||||||
<string name="permission_bluetooth_scan_label">Analyse Bluetooth</string>
|
<string name="permission_bluetooth_scan_label">Analyse Bluetooth</string>
|
||||||
<string name="permission_bluetooth_scan_description">L’autorisation « Analyse Bluetooth » permet à l’appli de découvrir et de réceptionner les données Bluetooth des appareils à proximité tels que vos AirPods.</string>
|
<string name="permission_bluetooth_scan_description">L’autorisation « Analyse Bluetooth » permet à l’appli de découvrir et de réceptionner les données Bluetooth des appareils à proximité tels que vos AirPods.</string>
|
||||||
|
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||||
<string name="permission_bluetooth_description">Cette appli exige l’autorisation « Bluetooth » pour se connecter aux appareils Bluetooth jumelés.</string>
|
<string name="permission_bluetooth_description">Cette appli exige l’autorisation « Bluetooth » pour se connecter aux appareils Bluetooth jumelés.</string>
|
||||||
<string name="permission_access_fine_location_label">Accéder à la position précise</string>
|
<string name="permission_access_fine_location_label">Accéder à la position précise</string>
|
||||||
<string name="permission_access_fine_location_description">CAPod utilise l’autorisation « position précise » pour recevoir les données « faible énergie » de Bluetooth. Votre casque-micro utilise la technologie « basse énergie » de Bluetooth pour diffuser son état. Cette appli n’utilisera PAS les données Bluetooth pour déterminer votre position.</string>
|
<string name="permission_access_fine_location_description">CAPod utilise l’autorisation « position précise » pour recevoir les données « faible énergie » de Bluetooth. Votre casque-micro utilise la technologie « basse énergie » de Bluetooth pour diffuser son état. Cette appli n’utilisera PAS les données Bluetooth pour déterminer votre position.</string>
|
||||||
@@ -48,6 +52,7 @@
|
|||||||
<string name="pods_none_label_short">Aucun appareil</string>
|
<string name="pods_none_label_short">Aucun appareil</string>
|
||||||
<string name="pods_charging_label">En charge</string>
|
<string name="pods_charging_label">En charge</string>
|
||||||
<string name="pods_inear_label">Dans l’oreille</string>
|
<string name="pods_inear_label">Dans l’oreille</string>
|
||||||
|
<string name="pods_microphone_label">Microphone</string>
|
||||||
<string name="pods_yours">Les vôtres</string>
|
<string name="pods_yours">Les vôtres</string>
|
||||||
<string name="headset_being_worn_label">Porté</string>
|
<string name="headset_being_worn_label">Porté</string>
|
||||||
<string name="pods_case_unknown_state">État inconnu</string>
|
<string name="pods_case_unknown_state">État inconnu</string>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?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="general_error_label">Erro</string>
|
<string name="general_error_label">Erro</string>
|
||||||
<string name="general_grant_permission_action">Conceder permissão</string>
|
<string name="general_grant_permission_action">Conceder permissão</string>
|
||||||
<string name="overview_nomaindevice_label">Nenhum dispositivo principal</string>
|
<string name="overview_nomaindevice_label">Nenhum dispositivo principal</string>
|
||||||
@@ -10,6 +12,7 @@
|
|||||||
<string name="permission_bluetooth_connect_description">Este aplicativo requer a permissão \"Conexão Bluetooth\" para interagir com dispositivos emparelhados e iniciar conexões.</string>
|
<string name="permission_bluetooth_connect_description">Este aplicativo requer a permissão \"Conexão Bluetooth\" para interagir com dispositivos emparelhados e iniciar conexões.</string>
|
||||||
<string name="permission_bluetooth_scan_label">Escaneando bluetooth</string>
|
<string name="permission_bluetooth_scan_label">Escaneando bluetooth</string>
|
||||||
<string name="permission_bluetooth_scan_description">A permissão \"Escaneamento de Bluetooth\" permite que este aplicativo descubra e receba dados de Bluetooth de dispositivos próximos, como seus AirPods.</string>
|
<string name="permission_bluetooth_scan_description">A permissão \"Escaneamento de Bluetooth\" permite que este aplicativo descubra e receba dados de Bluetooth de dispositivos próximos, como seus AirPods.</string>
|
||||||
|
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||||
<string name="permission_bluetooth_description">Este aplicativo requer a permissão \"Bluetooth\" para se conectar a dispositivos bluetooth emparelhados.</string>
|
<string name="permission_bluetooth_description">Este aplicativo requer a permissão \"Bluetooth\" para se conectar a dispositivos bluetooth emparelhados.</string>
|
||||||
<string name="permission_access_fine_location_label">Acesse a boa localização</string>
|
<string name="permission_access_fine_location_label">Acesse a boa localização</string>
|
||||||
<string name="permission_access_fine_location_description">O CAPod usa a permissão \"Boa localização\" para receber dados de Bluetooth Low Energy. Seus fones de ouvido usam a tecnologia Bluetooth Low Energy para transmitir seu status. Este aplicativo NÃO usará dados Bluetooth para determinar sua localização.</string>
|
<string name="permission_access_fine_location_description">O CAPod usa a permissão \"Boa localização\" para receber dados de Bluetooth Low Energy. Seus fones de ouvido usam a tecnologia Bluetooth Low Energy para transmitir seu status. Este aplicativo NÃO usará dados Bluetooth para determinar sua localização.</string>
|
||||||
|
|||||||
@@ -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<DualApplePods>("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<DualApplePods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
|
||||||
this shouldBe instanceOf<AirPodsPro>()
|
this shouldBe instanceOf<AirPodsPro>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `unknown AppleDevice`() = runBlockingTest {
|
fun `unknown AppleDevice`() = runTest {
|
||||||
create<ApplePods>("07 19 01 FF FF 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
|
create<ApplePods>("07 19 01 FF FF 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
|
||||||
this shouldBe instanceOf<UnknownAppleDevice>()
|
this shouldBe instanceOf<UnknownAppleDevice>()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
class BasicSingleApplePodsTest : BaseAirPodsTest() {
|
class BasicSingleApplePodsTest : BaseAirPodsTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test mapping`() = runBlockingTest {
|
fun `test mapping`() = runTest {
|
||||||
create<SingleApplePods>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") {
|
create<SingleApplePods>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") {
|
||||||
rawPrefix shouldBe 0x01.toUByte()
|
rawPrefix shouldBe 0x01.toUByte()
|
||||||
rawDeviceModel shouldBe 0x0520.toUShort()
|
rawDeviceModel shouldBe 0x0520.toUShort()
|
||||||
@@ -22,7 +22,7 @@ class BasicSingleApplePodsTest : BaseAirPodsTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test battery headset percent`() = runBlockingTest {
|
fun `test battery headset percent`() = runTest {
|
||||||
create<SingleApplePods>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") {
|
create<SingleApplePods>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") {
|
||||||
batteryHeadsetPercent shouldBe 0.5f
|
batteryHeadsetPercent shouldBe 0.5f
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.apple.airpods.HasStateDetectionAirPods
|
||||||
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<DualApplePods>("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()
|
||||||
rawDeviceModel shouldBe 0x0e20.toUShort()
|
rawDeviceModel shouldBe 0x0e20.toUShort()
|
||||||
@@ -23,14 +24,14 @@ 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<DualApplePods>("07 19 01 0E 20 >2B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00101011
|
// 00101011
|
||||||
// --^-----
|
// --^-----
|
||||||
isLeftPodMicrophone shouldBe true
|
isLeftPodMicrophone shouldBe true
|
||||||
isRightPodMicrophone shouldBe false
|
isRightPodMicrophone shouldBe false
|
||||||
}
|
}
|
||||||
create<DualAirPods>("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00001011
|
// 00001011
|
||||||
// --^-----
|
// --^-----
|
||||||
isLeftPodMicrophone shouldBe false
|
isLeftPodMicrophone shouldBe false
|
||||||
@@ -39,26 +40,26 @@ 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<DualApplePods>("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
|
||||||
// 765432¹0
|
// 765432¹0
|
||||||
isLeftPodInEar shouldBe true
|
isLeftPodInEar shouldBe true
|
||||||
}
|
}
|
||||||
create<DualAirPods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00100000
|
// 00100000
|
||||||
// 765432¹0
|
// 765432¹0
|
||||||
isLeftPodInEar shouldBe false
|
isLeftPodInEar shouldBe false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right Pod is primary
|
// Right Pod is primary
|
||||||
create<DualAirPods>("07 19 01 0E 20 >09< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >09< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00001001
|
// 00001001
|
||||||
// 7654³210
|
// 7654³210
|
||||||
isLeftPodInEar shouldBe true
|
isLeftPodInEar shouldBe true
|
||||||
}
|
}
|
||||||
create<DualAirPods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00000001
|
// 00000001
|
||||||
// 7654³210
|
// 7654³210
|
||||||
isLeftPodInEar shouldBe false
|
isLeftPodInEar shouldBe false
|
||||||
@@ -66,26 +67,26 @@ 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<DualApplePods>("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
|
||||||
// 7654³210
|
// 7654³210
|
||||||
isRightPodInEar shouldBe true
|
isRightPodInEar shouldBe true
|
||||||
}
|
}
|
||||||
create<DualAirPods>("07 19 01 0E 20 >21< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >21< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00100001
|
// 00100001
|
||||||
// 7654³210
|
// 7654³210
|
||||||
isRightPodInEar shouldBe false
|
isRightPodInEar shouldBe false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right Pod is primary
|
// Right Pod is primary
|
||||||
create<DualAirPods>("07 19 01 0E 20 >03< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >03< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00000011
|
// 00000011
|
||||||
// 765432¹0
|
// 765432¹0
|
||||||
isRightPodInEar shouldBe true
|
isRightPodInEar shouldBe true
|
||||||
}
|
}
|
||||||
create<DualAirPods>("07 19 01 0E 20 >01< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >01< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00000001
|
// 00000001
|
||||||
// 765432¹0
|
// 765432¹0
|
||||||
isRightPodInEar shouldBe false
|
isRightPodInEar shouldBe false
|
||||||
@@ -93,15 +94,15 @@ 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<DualApplePods>("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
|
||||||
batteryLeftPodPercent shouldBe 0.9f
|
batteryLeftPodPercent shouldBe 0.9f
|
||||||
batteryRightPodPercent shouldBe 0.8f
|
batteryRightPodPercent shouldBe 0.8f
|
||||||
}
|
}
|
||||||
// Left Pod primary
|
// Left Pod primary
|
||||||
create<DualAirPods>("07 19 01 0E 20 2B >89< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
|
create<DualApplePods>("07 19 01 0E 20 2B >89< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
|
||||||
// F8 11111000
|
// F8 11111000
|
||||||
batteryLeftPodPercent shouldBe 0.9f
|
batteryLeftPodPercent shouldBe 0.9f
|
||||||
batteryRightPodPercent shouldBe 0.8f
|
batteryRightPodPercent shouldBe 0.8f
|
||||||
@@ -109,18 +110,18 @@ 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
|
||||||
*/
|
*/
|
||||||
// This is the left
|
// This is the left
|
||||||
create<DualAirPods>("07 19 01 0E 20 51 89 >94< 52 00 00 F4 89 82 6D 3E 27 7F 26 62 57 D0 E2 A6 49 E9 35") {
|
create<DualApplePods>("07 19 01 0E 20 51 89 >94< 52 00 00 F4 89 82 6D 3E 27 7F 26 62 57 D0 E2 A6 49 E9 35") {
|
||||||
// 1001 0100
|
// 1001 0100
|
||||||
isLeftPodCharging shouldBe false
|
isLeftPodCharging shouldBe false
|
||||||
isRightPodCharging shouldBe true
|
isRightPodCharging shouldBe true
|
||||||
}
|
}
|
||||||
// This is the right
|
// This is the right
|
||||||
create<DualAirPods>("07 19 01 0E 20 31 98 >A4< 01 00 00 31 B9 A0 C4 80 CD D1 CF B9 3A 9A 6D 48 31 08 EB") {
|
create<DualApplePods>("07 19 01 0E 20 31 98 >A4< 01 00 00 31 B9 A0 C4 80 CD D1 CF B9 3A 9A 6D 48 31 08 EB") {
|
||||||
// 1010 0100
|
// 1010 0100
|
||||||
isLeftPodCharging shouldBe false
|
isLeftPodCharging shouldBe false
|
||||||
isRightPodCharging shouldBe true
|
isRightPodCharging shouldBe true
|
||||||
@@ -130,26 +131,26 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
|||||||
* Left pod is charging
|
* Left pod is charging
|
||||||
*/
|
*/
|
||||||
// This is the left
|
// This is the left
|
||||||
create<DualAirPods>("07 19 01 0E 20 71 98 >94< 52 00 05 A5 37 31 B2 BD 42 68 0C 64 FD 00 99 4A E5 3E F4") {
|
create<DualApplePods>("07 19 01 0E 20 71 98 >94< 52 00 05 A5 37 31 B2 BD 42 68 0C 64 FD 00 99 4A E5 3E F4") {
|
||||||
// 1001 0100
|
// 1001 0100
|
||||||
isLeftPodCharging shouldBe true
|
isLeftPodCharging shouldBe true
|
||||||
isRightPodCharging shouldBe false
|
isRightPodCharging shouldBe false
|
||||||
}
|
}
|
||||||
// This is the right
|
// This is the right
|
||||||
create<DualAirPods>("07 19 01 0E 20 11 89 >A4< 04 00 04 BA 79 1B C0 65 69 C6 9F 19 6E 37 7D 6D 86 8D D9") {
|
create<DualApplePods>("07 19 01 0E 20 11 89 >A4< 04 00 04 BA 79 1B C0 65 69 C6 9F 19 6E 37 7D 6D 86 8D D9") {
|
||||||
// 1010 0100
|
// 1010 0100
|
||||||
isLeftPodCharging shouldBe true
|
isLeftPodCharging shouldBe true
|
||||||
isRightPodCharging shouldBe false
|
isRightPodCharging shouldBe false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Both charging
|
// Both charging
|
||||||
create<DualAirPods>("07 19 01 0E 20 55 88 >B4< 59 00 05 4B FC DF 68 28 A5 45 52 65 9C FE 51 86 3A B5 DB") {
|
create<DualApplePods>("07 19 01 0E 20 55 88 >B4< 59 00 05 4B FC DF 68 28 A5 45 52 65 9C FE 51 86 3A B5 DB") {
|
||||||
// 1011 0100
|
// 1011 0100
|
||||||
isLeftPodCharging shouldBe true
|
isLeftPodCharging shouldBe true
|
||||||
isRightPodCharging shouldBe true
|
isRightPodCharging shouldBe true
|
||||||
}
|
}
|
||||||
// Both not charging
|
// Both not charging
|
||||||
create<DualAirPods>("07 19 01 0E 20 00 F8 >8F< 03 00 05 4C 0F A0 C4 05 24 DD EB AF 92 99 FD 54 B1 06 48") {
|
create<DualApplePods>("07 19 01 0E 20 00 F8 >8F< 03 00 05 4C 0F A0 C4 05 24 DD EB AF 92 99 FD 54 B1 06 48") {
|
||||||
// 1000 1111
|
// 1000 1111
|
||||||
isLeftPodCharging shouldBe false
|
isLeftPodCharging shouldBe false
|
||||||
isRightPodCharging shouldBe false
|
isRightPodCharging shouldBe false
|
||||||
@@ -157,89 +158,89 @@ 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<DualApplePods>("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
|
||||||
}
|
}
|
||||||
create<DualAirPods>("07 19 01 0E 20 75 A9 >F4< 51 00 05 A0 37 92 35 49 79 CC DC 27 94 8E FB 72 12 94 52") {
|
create<DualApplePods>("07 19 01 0E 20 75 A9 >F4< 51 00 05 A0 37 92 35 49 79 CC DC 27 94 8E FB 72 12 94 52") {
|
||||||
// 0101 0011
|
// 0101 0011
|
||||||
isCaseCharging shouldBe true
|
isCaseCharging shouldBe true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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<DualApplePods>("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
|
||||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
caseLidState shouldBe DualApplePods.LidState.OPEN
|
||||||
}
|
}
|
||||||
// Lid open, left pod in case
|
// Lid open, left pod in case
|
||||||
create<DualAirPods>("07 19 01 0E 20 51 9A 93 >31< 00 00 95 D0 A5 D7 E3 F4 F1 38 38 99 61 3B 57 95 37 B7") {
|
create<DualApplePods>("07 19 01 0E 20 51 9A 93 >31< 00 00 95 D0 A5 D7 E3 F4 F1 38 38 99 61 3B 57 95 37 B7") {
|
||||||
// 31 0011 0001
|
// 31 0011 0001
|
||||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
caseLidState shouldBe DualApplePods.LidState.OPEN
|
||||||
}
|
}
|
||||||
// Lid open, left pod in case
|
// Lid open, left pod in case
|
||||||
create<DualAirPods>("07 19 01 0E 20 71 A9 92 31 00 00 DB 48 7F 32 8C CE 80 6F D9 27 98 D6 76 45 9D 62") {
|
create<DualApplePods>("07 19 01 0E 20 71 A9 92 31 00 00 DB 48 7F 32 8C CE 80 6F D9 27 98 D6 76 45 9D 62") {
|
||||||
// 31 0011 0001
|
// 31 0011 0001
|
||||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
caseLidState shouldBe DualApplePods.LidState.OPEN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Lid just closed
|
// Lid just closed
|
||||||
create<DualAirPods>("07 19 01 0E 20 55 AA B4 >39< 00 00 08 A6 DB 99 E0 5E 14 85 E5 C2 0B 68 D7 FF C3 A1") {
|
create<DualApplePods>("07 19 01 0E 20 55 AA B4 >39< 00 00 08 A6 DB 99 E0 5E 14 85 E5 C2 0B 68 D7 FF C3 A1") {
|
||||||
// 39 0011 1001
|
// 39 0011 1001
|
||||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
caseLidState shouldBe DualApplePods.LidState.CLOSED
|
||||||
}
|
}
|
||||||
// Lid closed
|
// Lid closed
|
||||||
create<DualAirPods>("07 19 01 0E 20 55 AA B4 38 00 00 F3 F7 08 3B 98 09 C0 DD E4 BD BD 84 55 56 8B 81") {
|
create<DualApplePods>("07 19 01 0E 20 55 AA B4 38 00 00 F3 F7 08 3B 98 09 C0 DD E4 BD BD 84 55 56 8B 81") {
|
||||||
// 38 0011 1000
|
// 38 0011 1000
|
||||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
caseLidState shouldBe DualApplePods.LidState.CLOSED
|
||||||
}
|
}
|
||||||
// Lid closed, right pod in case
|
// Lid closed, right pod in case
|
||||||
create<DualAirPods>("07 19 01 0E 20 51 9A 93 >38< 00 00 3A D8 85 76 B0 91 48 31 DA FF 6C 4A 2B C2 67 F4") {
|
create<DualApplePods>("07 19 01 0E 20 51 9A 93 >38< 00 00 3A D8 85 76 B0 91 48 31 DA FF 6C 4A 2B C2 67 F4") {
|
||||||
// 38 0011 1000
|
// 38 0011 1000
|
||||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
caseLidState shouldBe DualApplePods.LidState.CLOSED
|
||||||
}
|
}
|
||||||
// Lid closed, left pod in case
|
// Lid closed, left pod in case
|
||||||
create<DualAirPods>("07 19 01 0E 20 71 A9 92 38 00 00 44 91 C4 8B 85 98 DD 55 4E 6A CA BC B5 CA 8D 37") {
|
create<DualApplePods>("07 19 01 0E 20 71 A9 92 38 00 00 44 91 C4 8B 85 98 DD 55 4E 6A CA BC B5 CA 8D 37") {
|
||||||
// 38 0011 1000
|
// 38 0011 1000
|
||||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
caseLidState shouldBe DualApplePods.LidState.CLOSED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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<HasStateDetectionAirPods>("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
|
||||||
state shouldBe DualAirPods.ConnectionState.DISCONNECTED
|
state shouldBe HasStateDetectionAirPods.ConnectionState.DISCONNECTED
|
||||||
}
|
}
|
||||||
// Connected idle
|
// Connected idle
|
||||||
create<DualAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >04< 1D 69 69 9C C2 51 F3 1F BF 6E 45 DA 90 4A A3 E3") {
|
create<HasStateDetectionAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >04< 1D 69 69 9C C2 51 F3 1F BF 6E 45 DA 90 4A A3 E3") {
|
||||||
// 39 0011 1001
|
// 39 0011 1001
|
||||||
state shouldBe DualAirPods.ConnectionState.IDLE
|
state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
|
||||||
}
|
}
|
||||||
// Connected and playing music
|
// Connected and playing music
|
||||||
create<DualAirPods>("07 19 01 0E 20 2B A9 8F 01 00 >05< 14 F7 CB 49 9F D3 B3 22 77 D2 22 F1 74 8C AC A6") {
|
create<HasStateDetectionAirPods>("07 19 01 0E 20 2B A9 8F 01 00 >05< 14 F7 CB 49 9F D3 B3 22 77 D2 22 F1 74 8C AC A6") {
|
||||||
// 38 0011 1000
|
// 38 0011 1000
|
||||||
state shouldBe DualAirPods.ConnectionState.MUSIC
|
state shouldBe HasStateDetectionAirPods.ConnectionState.MUSIC
|
||||||
}
|
}
|
||||||
// Connected and call active
|
// Connected and call active
|
||||||
create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >06< 0F 4B 43 25 E0 4A 73 63 14 22 C2 3C 89 13 BD 97") {
|
create<HasStateDetectionAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >06< 0F 4B 43 25 E0 4A 73 63 14 22 C2 3C 89 13 BD 97") {
|
||||||
// 38 0011 1000
|
// 38 0011 1000
|
||||||
state shouldBe DualAirPods.ConnectionState.CALL
|
state shouldBe HasStateDetectionAirPods.ConnectionState.CALL
|
||||||
}
|
}
|
||||||
// Connected and call active
|
// Connected and call active
|
||||||
create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >07< E7 DF 76 44 85 B5 30 F4 95 14 02 DC A1 A4 8A 09") {
|
create<HasStateDetectionAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >07< E7 DF 76 44 85 B5 30 F4 95 14 02 DC A1 A4 8A 09") {
|
||||||
// 38 0011 1000
|
// 38 0011 1000
|
||||||
state shouldBe DualAirPods.ConnectionState.RINGING
|
state shouldBe HasStateDetectionAirPods.ConnectionState.RINGING
|
||||||
}
|
}
|
||||||
// Switching?
|
// Switching?
|
||||||
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<HasStateDetectionAirPods>("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") {
|
||||||
// 38 0011 1000
|
// 38 0011 1000
|
||||||
state shouldBe DualAirPods.ConnectionState.HANGING_UP
|
state shouldBe HasStateDetectionAirPods.ConnectionState.HANGING_UP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
package eu.darken.capod.pods.core.apple.airpods
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
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()
|
||||||
@@ -34,11 +35,11 @@ class AirPodsGen1Test : BaseAirPodsTest() {
|
|||||||
isRightPodInEar shouldBe false
|
isRightPodInEar shouldBe false
|
||||||
batteryCasePercent shouldBe 0.6f
|
batteryCasePercent shouldBe 0.6f
|
||||||
|
|
||||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
caseLidState shouldBe DualApplePods.LidState.OPEN
|
||||||
|
|
||||||
state shouldBe DualAirPods.ConnectionState.DISCONNECTED
|
|
||||||
|
|
||||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.AIRPODS_GEN1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
package eu.darken.capod.pods.core.apple.airpods
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
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()
|
||||||
@@ -33,11 +34,13 @@ class AirPodsGen2Test : BaseAirPodsTest() {
|
|||||||
isRightPodInEar shouldBe true
|
isRightPodInEar shouldBe true
|
||||||
batteryCasePercent shouldBe null
|
batteryCasePercent shouldBe null
|
||||||
|
|
||||||
caseLidState shouldBe DualAirPods.LidState.NOT_IN_CASE
|
caseLidState shouldBe DualApplePods.LidState.NOT_IN_CASE
|
||||||
|
|
||||||
state shouldBe DualAirPods.ConnectionState.MUSIC
|
state shouldBe HasStateDetectionAirPods.ConnectionState.MUSIC
|
||||||
|
|
||||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.AIRPODS_GEN2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
package eu.darken.capod.pods.core.apple.airpods
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
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()
|
||||||
@@ -33,16 +34,18 @@ class AirPodsGen3Test : BaseAirPodsTest() {
|
|||||||
isRightPodInEar shouldBe false
|
isRightPodInEar shouldBe false
|
||||||
batteryCasePercent shouldBe 0.9f
|
batteryCasePercent shouldBe 0.9f
|
||||||
|
|
||||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
caseLidState shouldBe DualApplePods.LidState.OPEN
|
||||||
|
|
||||||
state shouldBe DualAirPods.ConnectionState.IDLE
|
state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
|
||||||
|
|
||||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.AIRPODS_GEN3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
@@ -55,9 +58,9 @@ class AirPodsGen3Test : BaseAirPodsTest() {
|
|||||||
isRightPodInEar shouldBe true
|
isRightPodInEar shouldBe true
|
||||||
batteryCasePercent shouldBe null
|
batteryCasePercent shouldBe null
|
||||||
|
|
||||||
caseLidState shouldBe DualAirPods.LidState.NOT_IN_CASE
|
caseLidState shouldBe DualApplePods.LidState.NOT_IN_CASE
|
||||||
|
|
||||||
state shouldBe DualAirPods.ConnectionState.UNKNOWN
|
state shouldBe HasStateDetectionAirPods.ConnectionState.UNKNOWN
|
||||||
|
|
||||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
package eu.darken.capod.pods.core.apple.airpods
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
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()
|
||||||
@@ -26,12 +27,14 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
|||||||
isHeadsetBeingCharged shouldBe false
|
isHeadsetBeingCharged shouldBe false
|
||||||
|
|
||||||
isHeadphonesBeingWorn shouldBe true
|
isHeadphonesBeingWorn shouldBe true
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.AIRPODS_MAX
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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()
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package eu.darken.capod.pods.core.apple.airpods
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
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 +13,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()
|
||||||
@@ -40,6 +41,8 @@ class AirPodsPro2Test : BaseAirPodsTest() {
|
|||||||
batteryCasePercent shouldBe 0.9f
|
batteryCasePercent shouldBe 0.9f
|
||||||
|
|
||||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.AIRPODS_PRO2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +50,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()
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
package eu.darken.capod.pods.core.apple.airpods
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
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()
|
||||||
@@ -34,11 +35,13 @@ class AirPodsProTest : BaseAirPodsTest() {
|
|||||||
batteryCasePercent shouldBe 0.5f
|
batteryCasePercent shouldBe 0.5f
|
||||||
|
|
||||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.AIRPODS_PRO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 +58,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 +162,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 +173,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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.beats
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
|
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class BeatsFitProTest : BaseAirPodsTest() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From https://github.com/d4rken-org/capod/issues/33#issuecomment-1256235651
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `test basics`() = runTest {
|
||||||
|
create<BeatsFitPro>("07 19 01 12 20 20 FA 8F 01 11 24 9B 9B 23 52 60 5A 8C 32 1C A5 C2 81 51 82 AF C8") {
|
||||||
|
rawPrefix shouldBe 0x01.toUByte()
|
||||||
|
rawDeviceModel shouldBe 0x1220.toUShort()
|
||||||
|
rawStatus shouldBe 0x20.toUByte()
|
||||||
|
rawPodsBattery shouldBe 0xFA.toUByte()
|
||||||
|
rawFlags shouldBe 0x8.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0xF.toUShort()
|
||||||
|
rawCaseLidState shouldBe 0x01.toUByte()
|
||||||
|
rawDeviceColor shouldBe 0x11.toUByte()
|
||||||
|
rawSuffix shouldBe 0x24.toUByte()
|
||||||
|
|
||||||
|
isLeftPodMicrophone shouldBe true
|
||||||
|
isRightPodMicrophone shouldBe false
|
||||||
|
|
||||||
|
batteryLeftPodPercent shouldBe 1.0f
|
||||||
|
batteryRightPodPercent shouldBe null
|
||||||
|
|
||||||
|
isCaseCharging shouldBe false
|
||||||
|
isRightPodCharging shouldBe false
|
||||||
|
isLeftPodCharging shouldBe false
|
||||||
|
batteryCasePercent shouldBe null
|
||||||
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.UNKNOWN.name
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.BEATS_FIT_PRO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
package eu.darken.capod.pods.core.apple.beats
|
package eu.darken.capod.pods.core.apple.beats
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
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()
|
||||||
@@ -22,11 +23,13 @@ class BeatsFlexText : BaseAirPodsTest() {
|
|||||||
rawSuffix shouldBe 0x00.toUByte()
|
rawSuffix shouldBe 0x00.toUByte()
|
||||||
|
|
||||||
batteryHeadsetPercent shouldBe 0.4f
|
batteryHeadsetPercent shouldBe 0.4f
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.BEATS_FLEX
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
package eu.darken.capod.pods.core.apple.beats
|
package eu.darken.capod.pods.core.apple.beats
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
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()
|
||||||
@@ -22,6 +23,8 @@ class BeatsSolo3Test : BaseAirPodsTest() {
|
|||||||
rawSuffix shouldBe 0x40.toUByte()
|
rawSuffix shouldBe 0x40.toUByte()
|
||||||
|
|
||||||
batteryHeadsetPercent shouldBe 0.4f
|
batteryHeadsetPercent shouldBe 0.4f
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.BEATS_SOLO_3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
package eu.darken.capod.pods.core.apple.beats
|
package eu.darken.capod.pods.core.apple.beats
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
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()
|
||||||
@@ -22,6 +23,8 @@ class BeatsStudio3Test : BaseAirPodsTest() {
|
|||||||
rawSuffix shouldBe 0x40.toUByte()
|
rawSuffix shouldBe 0x40.toUByte()
|
||||||
|
|
||||||
batteryHeadsetPercent shouldBe 0.4f
|
batteryHeadsetPercent shouldBe 0.4f
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.BEATS_STUDIO_3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
package eu.darken.capod.pods.core.apple.beats
|
package eu.darken.capod.pods.core.apple.beats
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
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()
|
||||||
@@ -22,6 +23,8 @@ class BeatsXTest : BaseAirPodsTest() {
|
|||||||
rawSuffix shouldBe 0x00.toUByte()
|
rawSuffix shouldBe 0x00.toUByte()
|
||||||
|
|
||||||
batteryHeadsetPercent shouldBe 0.5f
|
batteryHeadsetPercent shouldBe 0.5f
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.BEATS_X
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
package eu.darken.capod.pods.core.apple.beats
|
package eu.darken.capod.pods.core.apple.beats
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
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()
|
||||||
@@ -22,6 +23,8 @@ class PowerBeats3Test : BaseAirPodsTest() {
|
|||||||
rawSuffix shouldBe 0x40.toUByte()
|
rawSuffix shouldBe 0x40.toUByte()
|
||||||
|
|
||||||
batteryHeadsetPercent shouldBe 0.4f
|
batteryHeadsetPercent shouldBe 0.4f
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.POWERBEATS_3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
package eu.darken.capod.pods.core.apple.beats
|
package eu.darken.capod.pods.core.apple.beats
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
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()
|
||||||
@@ -34,6 +35,8 @@ class PowerBeatsProTest : BaseAirPodsTest() {
|
|||||||
batteryCasePercent shouldBe 0.5f
|
batteryCasePercent shouldBe 0.5f
|
||||||
|
|
||||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.POWERBEATS_PRO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class FakeAirPodsGen1Test : BaseAirPodsTest() {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `charging in box`() = runTest {
|
||||||
|
create<FakeAirPodsGen2>("07 13 01 0F 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00") {
|
||||||
|
rawPrefix shouldBe 0x01.toUByte()
|
||||||
|
rawDeviceModel shouldBe 0x0F20.toUShort()
|
||||||
|
rawStatus shouldBe 0x71.toUByte()
|
||||||
|
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||||
|
rawFlags shouldBe 0x3.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0x7.toUShort()
|
||||||
|
rawCaseLidState shouldBe 0x32.toUByte()
|
||||||
|
rawDeviceColor shouldBe 0x00.toUByte()
|
||||||
|
rawSuffix shouldBe 0x10.toUByte()
|
||||||
|
|
||||||
|
batteryLeftPodPercent shouldBe 1.0f
|
||||||
|
batteryRightPodPercent shouldBe 1.0f
|
||||||
|
|
||||||
|
isLeftPodInEar shouldBe false
|
||||||
|
isRightPodInEar shouldBe false
|
||||||
|
|
||||||
|
isLeftPodCharging shouldBe true
|
||||||
|
isRightPodCharging shouldBe true
|
||||||
|
|
||||||
|
isCaseCharging shouldBe false
|
||||||
|
|
||||||
|
batteryCasePercent shouldBe 0.7f
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.FAKE_AIRPODS_GEN2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
package eu.darken.capod.pods.core.apple.misc
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
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 FakeAirPodsGen2Test : 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<FakeAirPodsGen1>("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()
|
||||||
rawStatus shouldBe 0x71.toUByte()
|
rawStatus shouldBe 0x71.toUByte()
|
||||||
@@ -23,9 +24,17 @@ class Twsi99999Test : BaseAirPodsTest() {
|
|||||||
batteryLeftPodPercent shouldBe 1.0f
|
batteryLeftPodPercent shouldBe 1.0f
|
||||||
batteryRightPodPercent shouldBe 1.0f
|
batteryRightPodPercent shouldBe 1.0f
|
||||||
|
|
||||||
|
isLeftPodInEar shouldBe false
|
||||||
|
isRightPodInEar shouldBe false
|
||||||
|
|
||||||
|
isLeftPodCharging shouldBe true
|
||||||
|
isRightPodCharging shouldBe true
|
||||||
|
|
||||||
isCaseCharging shouldBe false
|
isCaseCharging shouldBe false
|
||||||
|
|
||||||
batteryCasePercent shouldBe 0.7f
|
batteryCasePercent shouldBe 0.7f
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.FAKE_AIRPODS_GEN1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class FakeAirPodsGen3Test : BaseAirPodsTest() {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `charging in case`() = runTest {
|
||||||
|
create<FakeAirPodsGen3>("07 13 01 13 20 75 AA 37 34 00 10 00 E4 E4 64 00 00 00 00 00 00") {
|
||||||
|
rawPrefix shouldBe 0x01.toUByte()
|
||||||
|
rawDeviceModel shouldBe 0x1320.toUShort()
|
||||||
|
rawStatus shouldBe 0x75.toUByte()
|
||||||
|
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||||
|
rawFlags shouldBe 0x3.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0x7.toUShort()
|
||||||
|
rawCaseLidState shouldBe 0x34.toUByte()
|
||||||
|
rawDeviceColor shouldBe 0x00.toUByte()
|
||||||
|
rawSuffix shouldBe 0x10.toUByte()
|
||||||
|
|
||||||
|
batteryLeftPodPercent shouldBe 1f
|
||||||
|
batteryRightPodPercent shouldBe 1f
|
||||||
|
|
||||||
|
isLeftPodInEar shouldBe false
|
||||||
|
isRightPodInEar shouldBe false
|
||||||
|
|
||||||
|
isCaseCharging shouldBe false
|
||||||
|
isLeftPodCharging shouldBe true
|
||||||
|
isRightPodCharging shouldBe true
|
||||||
|
|
||||||
|
batteryCasePercent shouldBe 0.7f
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.FAKE_AIRPODS_GEN3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class FakeAirPodsPro2Test : BaseAirPodsTest() {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `guessed data`() = runTest {
|
||||||
|
create<FakeAirPodsPro2>("07 13 01 14 20 75 AA 58 35 00 10 00 E4 E4 26 00 00 00 00 00 00") {
|
||||||
|
rawPrefix shouldBe 0x01.toUByte()
|
||||||
|
rawDeviceModel shouldBe 0x1420.toUShort()
|
||||||
|
rawStatus shouldBe 0x75.toUByte()
|
||||||
|
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||||
|
rawFlags shouldBe 0x5.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0x8.toUShort()
|
||||||
|
rawCaseLidState shouldBe 0x35.toUByte()
|
||||||
|
rawDeviceColor shouldBe 0x00.toUByte()
|
||||||
|
rawSuffix shouldBe 0x10.toUByte()
|
||||||
|
|
||||||
|
batteryLeftPodPercent shouldBe 1.0f
|
||||||
|
batteryRightPodPercent shouldBe 1.0f
|
||||||
|
|
||||||
|
isCaseCharging shouldBe true
|
||||||
|
|
||||||
|
batteryCasePercent shouldBe 0.8f
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.FAKE_AIRPODS_PRO2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://discord.com/channels/548521543039189022/927235844127993866/1063027552765087754
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `user supplied`() = runTest {
|
||||||
|
create<FakeAirPodsPro2>("07 13 01 14 20 75 AA 72 39 00 00 6F E4 E4 93 30 00 30 30 30 30") {
|
||||||
|
rawPrefix shouldBe 0x01.toUByte()
|
||||||
|
rawDeviceModel shouldBe 0x1420.toUShort()
|
||||||
|
rawStatus shouldBe 0x75.toUByte()
|
||||||
|
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||||
|
rawFlags shouldBe 0x7.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0x2.toUShort()
|
||||||
|
rawCaseLidState shouldBe 0x39.toUByte()
|
||||||
|
rawDeviceColor shouldBe 0x00.toUByte()
|
||||||
|
rawSuffix shouldBe 0x0.toUByte()
|
||||||
|
|
||||||
|
batteryLeftPodPercent shouldBe 1.0f
|
||||||
|
batteryRightPodPercent shouldBe 1.0f
|
||||||
|
|
||||||
|
isCaseCharging shouldBe true
|
||||||
|
|
||||||
|
batteryCasePercent shouldBe 0.2f
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.FAKE_AIRPODS_PRO2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
package eu.darken.capod.pods.core.apple.misc
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
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 FakeAirPodsProTest : 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<FakeAirPodsPro>("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()
|
||||||
rawStatus shouldBe 0x71.toUByte()
|
rawStatus shouldBe 0x71.toUByte()
|
||||||
@@ -23,9 +24,17 @@ class VarunrAirPodsProTest : BaseAirPodsTest() {
|
|||||||
batteryLeftPodPercent shouldBe 1.0f
|
batteryLeftPodPercent shouldBe 1.0f
|
||||||
batteryRightPodPercent shouldBe 1.0f
|
batteryRightPodPercent shouldBe 1.0f
|
||||||
|
|
||||||
|
isLeftPodCharging shouldBe true
|
||||||
|
isRightPodCharging shouldBe true
|
||||||
|
|
||||||
|
isLeftPodInEar shouldBe false
|
||||||
|
isRightPodInEar shouldBe false
|
||||||
|
|
||||||
isCaseCharging shouldBe false
|
isCaseCharging shouldBe false
|
||||||
|
|
||||||
batteryCasePercent shouldBe 0.7f
|
batteryCasePercent shouldBe 0.7f
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.FAKE_AIRPODS_PRO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import eu.darken.capod.R
|
|||||||
import eu.darken.capod.common.lists.binding
|
import eu.darken.capod.common.lists.binding
|
||||||
import eu.darken.capod.databinding.OverviewPodsDualItemBinding
|
import eu.darken.capod.databinding.OverviewPodsDualItemBinding
|
||||||
import eu.darken.capod.pods.core.*
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
class DualPodsCardVH(parent: ViewGroup) :
|
class DualPodsCardVH(parent: ViewGroup) :
|
||||||
@@ -29,14 +29,16 @@ class DualPodsCardVH(parent: ViewGroup) :
|
|||||||
}
|
}
|
||||||
text = sb
|
text = sb
|
||||||
|
|
||||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
|
||||||
else setTypeface(typeface, Typeface.NORMAL)
|
else setTypeface(null, Typeface.NORMAL)
|
||||||
|
|
||||||
if (device is DualAirPods && item.showDebug) {
|
if (device is DualApplePods && item.showDebug) {
|
||||||
append(" [${device.primaryPod.name}]")
|
append(" [${device.primaryPod.name}]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deviceIcon.setImageResource(device.iconRes)
|
deviceIcon.setImageResource(device.iconRes)
|
||||||
|
podLeftIcon.setImageResource(device.leftPodIcon)
|
||||||
|
podRightIcon.setImageResource(device.rightPodIcon)
|
||||||
|
|
||||||
lastSeen.text = device.lastSeenFormatted(item.now)
|
lastSeen.text = device.lastSeenFormatted(item.now)
|
||||||
|
|
||||||
@@ -91,6 +93,7 @@ class DualPodsCardVH(parent: ViewGroup) :
|
|||||||
// Case charge state
|
// Case charge state
|
||||||
device.apply {
|
device.apply {
|
||||||
if (this is HasCase) {
|
if (this is HasCase) {
|
||||||
|
podCaseIcon.setImageResource(caseIcon)
|
||||||
podCaseBatteryIcon.isGone = false
|
podCaseBatteryIcon.isGone = false
|
||||||
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
||||||
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ class SinglePodsCardVH(parent: ViewGroup) :
|
|||||||
|
|
||||||
name.apply {
|
name.apply {
|
||||||
text = device.getLabel(context)
|
text = device.getLabel(context)
|
||||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
|
||||||
else setTypeface(typeface, Typeface.NORMAL)
|
else setTypeface(null, Typeface.NORMAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceIcon.setImageResource(device.iconRes)
|
deviceIcon.setImageResource(device.iconRes)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:layout_marginBottom="2dp"
|
android:layout_marginBottom="2dp"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:srcCompat="@drawable/ic_airpod_both_24" />
|
app:srcCompat="@drawable/devic_airpods_gen1_both" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/name"
|
android:id="@+id/name"
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon"
|
||||||
android:layout_marginHorizontal="2dp"
|
android:layout_marginHorizontal="2dp"
|
||||||
android:contentDescription="@string/pods_dual_left_label"
|
android:contentDescription="@string/pods_dual_left_label"
|
||||||
android:src="@drawable/ic_airpod_left_24"
|
android:src="@drawable/devic_airpods_gen1_left"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/pod_left_wear_icon"
|
app:layout_constraintEnd_toStartOf="@+id/pod_left_wear_icon"
|
||||||
app:layout_constraintHorizontal_chainStyle="spread"
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_case_icon"
|
android:id="@+id/pod_case_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon"
|
||||||
android:src="@drawable/ic_airpod_case_24"
|
android:src="@drawable/devic_airpods_gen1_case"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon"
|
||||||
android:layout_marginHorizontal="2dp"
|
android:layout_marginHorizontal="2dp"
|
||||||
android:contentDescription="@string/pods_dual_right_label"
|
android:contentDescription="@string/pods_dual_right_label"
|
||||||
android:src="@drawable/ic_airpod_right_24"
|
android:src="@drawable/devic_airpods_gen1_right"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||||
app:layout_constraintStart_toEndOf="@+id/pod_right_wear_icon"
|
app:layout_constraintStart_toEndOf="@+id/pod_right_wear_icon"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:srcCompat="@drawable/ic_device_generic_headphones" />
|
app:srcCompat="@drawable/devic_headphones_generic" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/name"
|
android:id="@+id/name"
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
<style name="PodInfoItemIcon" parent="TextAppearance.Material3.BodySmall">
|
<style name="PodInfoItemIcon" parent="TextAppearance.Material3.BodySmall">
|
||||||
<item name="android:layout_height">16dp</item>
|
<item name="android:layout_height">16dp</item>
|
||||||
<item name="android:layout_width">16dp</item>
|
<item name="android:layout_width">16dp</item>
|
||||||
|
<item name="android:tintMode">multiply</item>
|
||||||
|
<item name="android:tint">?android:attr/colorAccent</item>
|
||||||
|
<item name="tint">?android:attr/colorAccent</item>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="PodInfoItemText" parent="TextAppearance.Material3.BodyMedium">
|
<style name="PodInfoItemText" parent="TextAppearance.Material3.BodyMedium">
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
val customProguardRules = fileTree(File("../proguard")) {
|
val customProguardRules = fileTree(File(projectDir, "proguard")) {
|
||||||
include("*.pro")
|
include("*.pro")
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources></resources>
|
<resources>
|
||||||
|
<string name="upgrades_gplay_unavailable_error">Los servicios de Google Play no están disponibles.</string>
|
||||||
|
<string name="upgrades_no_purchases_found_check_account">No se encontraron compras. ¿Está usando la cuenta correcta?</string>
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ import androidx.annotation.MenuRes
|
|||||||
import androidx.annotation.XmlRes
|
import androidx.annotation.XmlRes
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import eu.darken.capod.common.preferences.Settings
|
import eu.darken.capod.common.preferences.Settings
|
||||||
import eu.darken.capod.main.ui.settings.SettingsFragment
|
import eu.darken.capod.main.ui.settings.SettingsFragment
|
||||||
|
|
||||||
@@ -69,17 +67,4 @@ abstract class PreferenceFragment2
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <T> LiveData<T>.observe2(
|
|
||||||
crossinline callback: (T) -> Unit
|
|
||||||
) {
|
|
||||||
observe(viewLifecycleOwner) { callback.invoke(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <T, reified VB : ViewBinding?> LiveData<T>.observe2(
|
|
||||||
ui: VB,
|
|
||||||
crossinline callback: VB.(T) -> Unit
|
|
||||||
) {
|
|
||||||
observe(viewLifecycleOwner) { callback.invoke(ui, it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package eu.darken.capod.common.uix
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import eu.darken.capod.common.error.asErrorDialogBuilder
|
||||||
|
|
||||||
|
abstract class PreferenceFragment3 : PreferenceFragment2() {
|
||||||
|
|
||||||
|
abstract val vm: ViewModel3
|
||||||
|
|
||||||
|
var onErrorEvent: ((Throwable) -> Boolean)? = null
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
vm.errorEvents.observe2 {
|
||||||
|
val showDialog = onErrorEvent?.invoke(it) ?: true
|
||||||
|
if (showDialog) it.asErrorDialogBuilder(requireContext()).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> LiveData<T>.observe2(
|
||||||
|
crossinline callback: (T) -> Unit
|
||||||
|
) {
|
||||||
|
observe(viewLifecycleOwner) { callback.invoke(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T, reified VB : ViewBinding?> LiveData<T>.observe2(
|
||||||
|
ui: VB,
|
||||||
|
crossinline callback: VB.(T) -> Unit
|
||||||
|
) {
|
||||||
|
observe(viewLifecycleOwner) { callback.invoke(ui, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||