mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
454d76e305 | ||
|
|
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.
|
||||
* Show popup when case is opened.
|
||||
* Support for Wear OS
|
||||
* Widgets
|
||||
|
||||
CAPod is ad-free. Some additional features require an in-app purchase.
|
||||
|
||||
@@ -36,9 +37,13 @@ Currently supported models:
|
||||
|
||||
## Download
|
||||
|
||||
* [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod)
|
||||
* [GitHub](https://github.com/d4rken-org/capod/releases/latest)
|
||||
* [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.darken.capod)
|
||||
| Source | Status |
|
||||
|-----------------------|--------|
|
||||
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [](https://play.google.com/store/apps/details?id=eu.darken.capod) |
|
||||
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | ? |
|
||||
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [](https://github.com/d4rken-org/capod/releases/latest) |
|
||||
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [](https://apt.izzysoft.de/packages/eu.darken.capod/) |
|
||||
|
||||
|
||||
## Support the project
|
||||
|
||||
@@ -54,7 +59,7 @@ Currently supported models:
|
||||
## 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://raw.githubusercontent.com/d4rken-org/capod/main/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" width="200">
|
||||
## Thanks to
|
||||
|
||||
* 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)" }
|
||||
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 {
|
||||
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" }
|
||||
} && !compatMode
|
||||
|
||||
val scanner = bluetoothManager.scanner
|
||||
val scanner = bluetoothManager.scanner ?: throw IllegalStateException("BLE scanner unavailable")
|
||||
|
||||
val callback = object : ScanCallback() {
|
||||
var lastScanAt = System.currentTimeMillis()
|
||||
|
||||
@@ -30,12 +30,11 @@ class BluetoothManager2 @Inject constructor(
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
) {
|
||||
|
||||
val adapter: BluetoothAdapter
|
||||
val adapter: BluetoothAdapter?
|
||||
get() = manager.adapter
|
||||
|
||||
val scanner: BluetoothLeScanner
|
||||
get() = adapter.bluetoothLeScanner
|
||||
?: throw IllegalStateException("Bluetooth is disabled or permissiong missing")
|
||||
val scanner: BluetoothLeScanner?
|
||||
get() = adapter?.bluetoothLeScanner
|
||||
|
||||
val isBluetoothEnabled: Flow<Boolean> = callbackFlow {
|
||||
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 ->
|
||||
try {
|
||||
|
||||
@@ -7,6 +7,7 @@ import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.error.hasCause
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlin.time.Duration
|
||||
|
||||
@@ -71,4 +72,11 @@ fun <T> Flow<T>.setupCommonEventHandlers(tag: String, identifier: () -> String)
|
||||
log(tag, ERROR) { "${identifier()} failed: ${it.asLog()}" }
|
||||
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.logTag
|
||||
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.PermissionTool
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
@@ -79,6 +80,7 @@ class PodMonitor @Inject constructor(
|
||||
|
||||
val mainDevice: Flow<PodDevice?> = devices
|
||||
.map { determineMainDevice(it) }
|
||||
.setupCommonEventHandlers(TAG) { "mainDevice" }
|
||||
.replayingShare(appScope)
|
||||
|
||||
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 {
|
||||
return ScanFilter.Builder().build()
|
||||
}
|
||||
|
||||
@@ -104,11 +104,14 @@ interface PodDevice {
|
||||
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
|
||||
"Power Beats Pro"
|
||||
),
|
||||
@Json(name = "fakes.tws.i99999") TWS_I99999(
|
||||
"TWS i99999"
|
||||
@Json(name = "fakes.tws.i99999") FAKE_AIRPODS_GEN1(
|
||||
"AirPods (Gen 1)? \uD83C\uDFAD"
|
||||
),
|
||||
@Json(name = "fakes.varunr.airpodspro") VARUNR_AIRPODS_PRO(
|
||||
"Fake AirPods Pro"
|
||||
@Json(name = "fakes.varunr.airpodspro") FAKE_AIRPODS_PRO(
|
||||
"AirPods Pro? \uD83C\uDFAD"
|
||||
),
|
||||
@Json(name = "fakes.generic.airpods.gen3") FAKE_AIRPODS_GEN3(
|
||||
"AirPods (Gen 3)? \uD83C\uDFAD"
|
||||
),
|
||||
@Json(name = "unknown") UNKNOWN(
|
||||
"Unknown"
|
||||
|
||||
@@ -7,8 +7,9 @@ import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import eu.darken.capod.pods.core.apple.airpods.*
|
||||
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.VarunrAirPodsPro
|
||||
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen1
|
||||
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen3
|
||||
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsPro
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
@@ -29,7 +30,7 @@ abstract class AppleFactoryModule {
|
||||
@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 fakesTwsi999999(factory: Twsi99999.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet
|
||||
abstract fun fakesVarunrAirPodsPro(factory: VarunrAirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun fakeAirPodsGen1(factory: FakeAirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun fakeAirPodsPro(factory: FakeAirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun fakeAirPodsGen3(factory: FakeAirPodsGen3.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.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.lowerNibble
|
||||
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()})"
|
||||
|
||||
companion object {
|
||||
const val MAX_HISTORY = 10
|
||||
const val MAX_HISTORY = 20
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +89,22 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
?.caseLidState
|
||||
}
|
||||
|
||||
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? {
|
||||
val scanResult = current.scanResult
|
||||
val message = current.proximityMessage
|
||||
@@ -103,7 +120,7 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
.filter { it.history.size > KnownDevice.MAX_HISTORY }
|
||||
.toList()
|
||||
.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
|
||||
@@ -118,7 +135,7 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
}
|
||||
|
||||
if (recognizedDevice == null) {
|
||||
log(tag) { "searchHistory1: Didn't recognize: $message" }
|
||||
log(tag, WARN) { "searchHistory1: Didn't recognize: $message" }
|
||||
}
|
||||
|
||||
return recognizedDevice
|
||||
|
||||
+6
-5
@@ -19,8 +19,9 @@ import javax.inject.Inject
|
||||
/**
|
||||
* Basically an AirPods GEN1 clone
|
||||
* 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 seenLastAt: Instant = Instant.now(),
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
@@ -32,7 +33,7 @@ data class Twsi99999 constructor(
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.TWS_I99999
|
||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN1
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||
@@ -109,7 +110,7 @@ data class Twsi99999 constructor(
|
||||
override val isCaseCharging: Boolean
|
||||
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 {
|
||||
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||
@@ -117,7 +118,7 @@ data class Twsi99999 constructor(
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
@@ -139,6 +140,6 @@ data class Twsi99999 constructor(
|
||||
|
||||
companion object {
|
||||
private val DEVICE_CODE = 0x0220.toUShort()
|
||||
private val TAG = logTag("PodDevice", "Apple", "TWS", "i99999")
|
||||
private val TAG = logTag("PodDevice", "Apple", "Fake", "AirPods", "Gen1")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
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, HasDualMicrophone, HasCase, HasChargeDetectionDual {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
val isThisPodInThecase: Boolean
|
||||
get() = rawStatus.isBitSet(6)
|
||||
|
||||
/**
|
||||
* The data flip bit is set if the left pod is primary.
|
||||
* For the pod that is in the case, this is flipped again though.
|
||||
*/
|
||||
override val isLeftPodMicrophone: Boolean
|
||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||
|
||||
/**
|
||||
* The data flip bit is UNset if the right pod is primary.
|
||||
* For the pod that is in the case, this is flipped again though.
|
||||
*/
|
||||
override val isRightPodMicrophone: Boolean
|
||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -19,8 +19,9 @@ import javax.inject.Inject
|
||||
/**
|
||||
* AirPods Pro clone similar to Twsi999999.
|
||||
* Shorter data structure.
|
||||
* https://discord.com/channels/548521543039189022/927235844127993866/962068944196358234
|
||||
*/
|
||||
data class VarunrAirPodsPro constructor(
|
||||
data class FakeAirPodsPro constructor(
|
||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||
override val seenLastAt: Instant = Instant.now(),
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
@@ -32,7 +33,7 @@ data class VarunrAirPodsPro constructor(
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.VARUNR_AIRPODS_PRO
|
||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||
@@ -109,7 +110,7 @@ data class VarunrAirPodsPro constructor(
|
||||
override val isCaseCharging: Boolean
|
||||
get() = rawFlags.isBitSet(2)
|
||||
|
||||
class Factory @Inject constructor() : ApplePodsFactory<VarunrAirPodsPro>(TAG) {
|
||||
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?
|
||||
@@ -117,7 +118,7 @@ data class VarunrAirPodsPro constructor(
|
||||
}
|
||||
|
||||
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||
var basic = VarunrAirPodsPro(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = FakeAirPodsPro(scanResult = scanResult, proximityMessage = message)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
@@ -139,6 +140,6 @@ data class VarunrAirPodsPro constructor(
|
||||
|
||||
companion object {
|
||||
private val DEVICE_CODE = 0x0E20.toUShort()
|
||||
private val TAG = logTag("PodDevice", "Apple", "Varunr", "AirPodsPro")
|
||||
private val TAG = logTag("PodDevice", "Apple", "Fake", "AirPods", "Pro")
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,8 @@ class AutoConnect @Inject constructor(
|
||||
return@map
|
||||
}
|
||||
|
||||
val bondedDevice = bluetoothManager.bondedDevices().firstOrNull { it.address == mainDeviceAddr }
|
||||
val bondedDevice = bluetoothManager.bondedDevices().first().firstOrNull { it.address == mainDeviceAddr }
|
||||
|
||||
if (bondedDevice == null) {
|
||||
log(TAG, WARN) { "No bonded device matches $mainDeviceAddr" }
|
||||
return@map
|
||||
|
||||
@@ -1,2 +1,10 @@
|
||||
<?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"?>
|
||||
<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_error_label">Erreur</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_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_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_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>
|
||||
@@ -48,6 +52,7 @@
|
||||
<string name="pods_none_label_short">Aucun appareil</string>
|
||||
<string name="pods_charging_label">En charge</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="headset_being_worn_label">Porté</string>
|
||||
<string name="pods_case_unknown_state">État inconnu</string>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_error_label">Error</string>
|
||||
<string name="general_grant_permission_action">Berikan izin</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>
|
||||
@@ -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_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_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_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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_grant_permission_action">Berikan kebenaran</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_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_label">Bluetooth</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_description">CAPod menggunakan kebenaran \"lokasi baik\" untuk menerima data Bluetooth Tenaga Rendah. Fon kepala anda menggunakan teknologi Bluetooth Tenaga Rendah untuk menyiarkan statusnya. Apl ini TIDAK akan menggunakan data Bluetooth untuk menentukan lokasi anda.</string>
|
||||
|
||||
+4
-4
@@ -5,7 +5,7 @@ import com.squareup.moshi.Moshi
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
import testhelpers.json.toComparableJson
|
||||
@@ -25,7 +25,7 @@ class FlowPreferenceMoshiTest : BaseTest() {
|
||||
)
|
||||
|
||||
@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 testData2 = TestGson(string = "update")
|
||||
val moshi = Moshi.Builder().build()
|
||||
@@ -67,7 +67,7 @@ class FlowPreferenceMoshiTest : BaseTest() {
|
||||
}
|
||||
|
||||
@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 testData2 = TestGson(string = "update")
|
||||
val moshi = Moshi.Builder().build()
|
||||
@@ -109,7 +109,7 @@ class FlowPreferenceMoshiTest : BaseTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `enum serialization`() = runBlockingTest {
|
||||
fun `enum serialization`() = runTest {
|
||||
val moshi = Moshi.Builder().build()
|
||||
val monitorMode = mockPreferences.createFlowPreference(
|
||||
"core.monitor.mode",
|
||||
|
||||
@@ -2,7 +2,7 @@ package eu.darken.capod.common.preferences
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
import testhelpers.preferences.MockSharedPreferences
|
||||
@@ -12,7 +12,7 @@ class FlowPreferenceTest : BaseTest() {
|
||||
private val mockPreferences = MockSharedPreferences()
|
||||
|
||||
@Test
|
||||
fun `reading and writing strings`() = runBlockingTest {
|
||||
fun `reading and writing strings`() = runTest {
|
||||
mockPreferences.createFlowPreference<String?>(
|
||||
key = "testKey",
|
||||
defaultValue = "default"
|
||||
@@ -41,7 +41,7 @@ class FlowPreferenceTest : BaseTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reading and writing boolean`() = runBlockingTest {
|
||||
fun `reading and writing boolean`() = runTest {
|
||||
mockPreferences.createFlowPreference<Boolean?>(
|
||||
key = "testKey",
|
||||
defaultValue = true
|
||||
@@ -70,7 +70,7 @@ class FlowPreferenceTest : BaseTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reading and writing long`() = runBlockingTest {
|
||||
fun `reading and writing long`() = runTest {
|
||||
mockPreferences.createFlowPreference<Long?>(
|
||||
key = "testKey",
|
||||
defaultValue = 9000L
|
||||
@@ -99,7 +99,7 @@ class FlowPreferenceTest : BaseTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reading and writing integer`() = runBlockingTest {
|
||||
fun `reading and writing integer`() = runTest {
|
||||
mockPreferences.createFlowPreference<Long?>(
|
||||
key = "testKey",
|
||||
defaultValue = 123
|
||||
@@ -128,7 +128,7 @@ class FlowPreferenceTest : BaseTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reading and writing float`() = runBlockingTest {
|
||||
fun `reading and writing float`() = runTest {
|
||||
mockPreferences.createFlowPreference<Float?>(
|
||||
key = "testKey",
|
||||
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.types.instanceOf
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsFactoryTest : BaseAirPodsTest() {
|
||||
|
||||
@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") {
|
||||
this shouldBe instanceOf<AirPodsGen1>()
|
||||
}
|
||||
}
|
||||
|
||||
@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") {
|
||||
this shouldBe instanceOf<AirPodsPro>()
|
||||
}
|
||||
}
|
||||
|
||||
@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") {
|
||||
this shouldBe instanceOf<UnknownAppleDevice>()
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BasicSingleApplePodsTest : BaseAirPodsTest() {
|
||||
|
||||
@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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0520.toUShort()
|
||||
@@ -22,7 +22,7 @@ class BasicSingleApplePodsTest : BaseAirPodsTest() {
|
||||
}
|
||||
|
||||
@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") {
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DualApplePodsTest : BaseAirPodsTest() {
|
||||
|
||||
@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") {
|
||||
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
@@ -23,7 +23,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
}
|
||||
|
||||
@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") {
|
||||
// 00101011
|
||||
// --^-----
|
||||
@@ -39,7 +39,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - left pod ear status`() = runBlockingTest {
|
||||
fun `test AirPodDevice - left pod ear status`() = runTest {
|
||||
// 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") {
|
||||
// 00100010
|
||||
@@ -66,7 +66,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - right pod ear status`() = runBlockingTest {
|
||||
fun `test AirPodDevice - right pod ear status`() = runTest {
|
||||
// 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") {
|
||||
// 00101001
|
||||
@@ -93,7 +93,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - battery status`() = runBlockingTest {
|
||||
fun `test AirPodDevice - battery status`() = runTest {
|
||||
// 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") {
|
||||
// 88 10001000
|
||||
@@ -109,7 +109,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - pod charging`() = runBlockingTest {
|
||||
fun `test AirPodDevice - pod charging`() = runTest {
|
||||
/**
|
||||
* Right pod is charging
|
||||
*/
|
||||
@@ -157,7 +157,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
}
|
||||
|
||||
@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") {
|
||||
// 0011 0011
|
||||
isCaseCharging shouldBe false
|
||||
@@ -169,7 +169,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - case lid test`() = runBlockingTest {
|
||||
fun `test AirPodDevice - case lid test`() = runTest {
|
||||
// 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") {
|
||||
// 31 0011 0001
|
||||
@@ -210,7 +210,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - connection state`() = runBlockingTest {
|
||||
fun `test AirPodDevice - connection state`() = runTest {
|
||||
// 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") {
|
||||
// 31 0011 0001
|
||||
|
||||
@@ -2,13 +2,13 @@ package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsMax
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SingleApplePodsTest : BaseAirPodsTest() {
|
||||
|
||||
@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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0A20.toUShort()
|
||||
@@ -23,7 +23,7 @@ class SingleApplePodsTest : BaseAirPodsTest() {
|
||||
}
|
||||
|
||||
@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") {
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
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.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsGen1Test : BaseAirPodsTest() {
|
||||
|
||||
// Test data from https://github.com/adolfintel/OpenPods/issues/39#issuecomment-557664269
|
||||
@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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0220.toUShort()
|
||||
@@ -39,6 +40,8 @@ class AirPodsGen1Test : BaseAirPodsTest() {
|
||||
state shouldBe DualAirPods.ConnectionState.DISCONNECTED
|
||||
|
||||
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
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsGen2Test : BaseAirPodsTest() {
|
||||
|
||||
@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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0F20.toUShort()
|
||||
@@ -38,6 +39,8 @@ class AirPodsGen2Test : BaseAirPodsTest() {
|
||||
state shouldBe DualAirPods.ConnectionState.MUSIC
|
||||
|
||||
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
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsGen3Test : BaseAirPodsTest() {
|
||||
|
||||
@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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1320.toUShort()
|
||||
@@ -38,11 +39,13 @@ class AirPodsGen3Test : BaseAirPodsTest() {
|
||||
state shouldBe DualAirPods.ConnectionState.IDLE
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_GEN3
|
||||
}
|
||||
}
|
||||
|
||||
@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") {
|
||||
batteryLeftPodPercent shouldBe 0.8f
|
||||
batteryRightPodPercent shouldBe 0.8f
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsMaxTest : BaseAirPodsTest() {
|
||||
|
||||
// Test data from https://github.com/adolfintel/OpenPods/issues/124
|
||||
@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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0A20.toUShort()
|
||||
@@ -26,12 +27,14 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
isHeadphonesBeingWorn shouldBe true
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_MAX
|
||||
}
|
||||
}
|
||||
|
||||
// Test data from https://github.com/adolfintel/OpenPods/issues/124
|
||||
@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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0A20.toUShort()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsPro2Test : BaseAirPodsTest() {
|
||||
@@ -12,7 +13,7 @@ class AirPodsPro2Test : BaseAirPodsTest() {
|
||||
* https://github.com/d4rken-org/capod/issues/31#issuecomment-1256791084
|
||||
*/
|
||||
@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") {
|
||||
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
@@ -40,6 +41,8 @@ class AirPodsPro2Test : BaseAirPodsTest() {
|
||||
batteryCasePercent shouldBe 0.9f
|
||||
|
||||
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
|
||||
*/
|
||||
@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") {
|
||||
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
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.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsProTest : BaseAirPodsTest() {
|
||||
|
||||
@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") {
|
||||
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
@@ -34,11 +35,13 @@ class AirPodsProTest : BaseAirPodsTest() {
|
||||
batteryCasePercent shouldBe 0.5f
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_PRO
|
||||
}
|
||||
}
|
||||
|
||||
@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") {
|
||||
isLeftPodMicrophone shouldBe false
|
||||
isRightPodMicrophone shouldBe true
|
||||
@@ -55,7 +58,7 @@ class AirPodsProTest : BaseAirPodsTest() {
|
||||
|
||||
// Test data from https://github.com/adolfintel/OpenPods/issues/34#issuecomment-565894487
|
||||
@Test
|
||||
fun `various AirPods Pro messages`() = runBlockingTest {
|
||||
fun `various AirPods Pro messages`() = runTest {
|
||||
create<AirPodsPro>("0719010e202b668f01000500000000000000000000000000000000") {
|
||||
batteryLeftPodPercent 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
|
||||
fun `fake airpods`() = runBlockingTest {
|
||||
fun `fake airpods`() = runTest {
|
||||
create<AirPodsGen1>("071901022055AA563100006FE4DF10AF106081033B76D9C7112288") {
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
@@ -170,4 +173,19 @@ class AirPodsProTest : BaseAirPodsTest() {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsFlexText : BaseAirPodsTest() {
|
||||
|
||||
// Raw data from https://github.com/adolfintel/OpenPods/issues/105
|
||||
@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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1020.toUShort()
|
||||
@@ -22,11 +23,13 @@ class BeatsFlexText : BaseAirPodsTest() {
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
model shouldBe PodDevice.Model.BEATS_FLEX
|
||||
}
|
||||
}
|
||||
|
||||
@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") {
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.6f
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsSolo3Test : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0620.toUShort()
|
||||
@@ -22,6 +23,8 @@ class BeatsSolo3Test : BaseAirPodsTest() {
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
model shouldBe PodDevice.Model.BEATS_SOLO_3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsStudio3Test : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0920.toUShort()
|
||||
@@ -22,6 +23,8 @@ class BeatsStudio3Test : BaseAirPodsTest() {
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
model shouldBe PodDevice.Model.BEATS_STUDIO_3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsXTest : BaseAirPodsTest() {
|
||||
|
||||
// Raw data from https://github.com/adolfintel/OpenPods/issues/105
|
||||
@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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0520.toUShort()
|
||||
@@ -22,6 +23,8 @@ class BeatsXTest : BaseAirPodsTest() {
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
|
||||
model shouldBe PodDevice.Model.BEATS_X
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PowerBeats3Test : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0320.toUShort()
|
||||
@@ -22,6 +23,8 @@ class PowerBeats3Test : BaseAirPodsTest() {
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
model shouldBe PodDevice.Model.POWERBEATS_3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
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.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PowerBeatsProTest : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@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") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0B20.toUShort()
|
||||
@@ -34,6 +35,8 @@ class PowerBeatsProTest : BaseAirPodsTest() {
|
||||
batteryCasePercent shouldBe 0.5f
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.POWERBEATS_PRO
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -1,15 +1,16 @@
|
||||
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.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class Twsi99999Test : BaseAirPodsTest() {
|
||||
class FakeAirPodsGen1Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `charging in box`() = runBlockingTest {
|
||||
create<Twsi99999>("07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00") {
|
||||
fun `charging in box`() = runTest {
|
||||
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()
|
||||
rawDeviceModel shouldBe 0x0220.toUShort()
|
||||
rawStatus shouldBe 0x71.toUByte()
|
||||
@@ -26,6 +27,8 @@ class Twsi99999Test : BaseAirPodsTest() {
|
||||
isCaseCharging shouldBe false
|
||||
|
||||
batteryCasePercent shouldBe 0.7f
|
||||
|
||||
model shouldBe PodDevice.Model.FAKE_AIRPODS_GEN1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
|
||||
batteryCasePercent shouldBe 0.7f
|
||||
|
||||
model shouldBe PodDevice.Model.FAKE_AIRPODS_GEN3
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -1,15 +1,16 @@
|
||||
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.runBlockingTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class VarunrAirPodsProTest : BaseAirPodsTest() {
|
||||
class FakeAirPodsProTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `guessed data`() = runBlockingTest {
|
||||
create<VarunrAirPodsPro>("07 13 01 0E 20 71 AA 37 36 00 10 00 FF 64 FF 00 00 00 00 00 00") {
|
||||
fun `guessed data`() = runTest {
|
||||
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()
|
||||
rawDeviceModel shouldBe 0x0E20.toUShort()
|
||||
rawStatus shouldBe 0x71.toUByte()
|
||||
@@ -26,6 +27,8 @@ class VarunrAirPodsProTest : BaseAirPodsTest() {
|
||||
isCaseCharging shouldBe false
|
||||
|
||||
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.log
|
||||
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.permissions.Permission
|
||||
import eu.darken.capod.common.uix.ViewModel3
|
||||
@@ -76,8 +77,8 @@ class OverviewFragmentVM @Inject constructor(
|
||||
permissionTool.missingPermissions,
|
||||
debugSettings.isDebugModeEnabled.flow,
|
||||
bluetoothManager.isBluetoothEnabled,
|
||||
podMonitor.mainDevice,
|
||||
) { _, permissions, isDebugMode, isBluetoothEnabled, mainPod ->
|
||||
podMonitor.mainDevice.throttleLatest(1000),
|
||||
) { _, permissions, isDebugMode, isBluetoothEnabled, _ ->
|
||||
val items = mutableListOf<OverviewAdapter.Item>()
|
||||
|
||||
if (permissions.isNotEmpty()) {
|
||||
@@ -97,10 +98,7 @@ class OverviewFragmentVM @Inject constructor(
|
||||
return@combine items
|
||||
}
|
||||
|
||||
val podToShow = mainPod ?: podDeviceCache.loadMainDevice()?.let {
|
||||
log(TAG, VERBOSE) { "Using podDeviceCache: $it" }
|
||||
podFactory.createPod(it)?.device
|
||||
}
|
||||
val podToShow = podMonitor.latestMainDevice()
|
||||
log(TAG, VERBOSE) { "Showing $podToShow" }
|
||||
|
||||
val now = Instant.now()
|
||||
|
||||
+2
-2
@@ -29,8 +29,8 @@ class DualPodsCardVH(parent: ViewGroup) :
|
||||
}
|
||||
text = sb
|
||||
|
||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
||||
else setTypeface(typeface, Typeface.NORMAL)
|
||||
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
|
||||
else setTypeface(null, Typeface.NORMAL)
|
||||
|
||||
if (device is DualAirPods && item.showDebug) {
|
||||
append(" [${device.primaryPod.name}]")
|
||||
|
||||
+2
-2
@@ -23,8 +23,8 @@ class SinglePodsCardVH(parent: ViewGroup) :
|
||||
|
||||
name.apply {
|
||||
text = device.getLabel(context)
|
||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
||||
else setTypeface(typeface, Typeface.NORMAL)
|
||||
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
|
||||
else setTypeface(null, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
deviceIcon.setImageResource(device.iconRes)
|
||||
|
||||
@@ -51,7 +51,7 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
val customProguardRules = fileTree(File("../proguard")) {
|
||||
val customProguardRules = fileTree(File(projectDir, "proguard")) {
|
||||
include("*.pro")
|
||||
}
|
||||
debug {
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
<?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:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
@@ -69,6 +68,18 @@
|
||||
</intent-filter>
|
||||
</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
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
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.debug.autoreport.AutoReporting
|
||||
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 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 javax.inject.Inject
|
||||
|
||||
@@ -19,6 +27,9 @@ open class App : Application(), Configuration.Provider {
|
||||
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||
@Inject lateinit var autoReporting: AutoReporting
|
||||
@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
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -36,6 +47,24 @@ open class App : Application(), Configuration.Provider {
|
||||
appScope.launch {
|
||||
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()
|
||||
|
||||
@@ -10,9 +10,7 @@ import androidx.annotation.MenuRes
|
||||
import androidx.annotation.XmlRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import eu.darken.capod.common.preferences.Settings
|
||||
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) }
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
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.navigation.navVia
|
||||
import eu.darken.capod.common.permissions.Permission
|
||||
@@ -96,6 +97,7 @@ class OverviewFragmentVM @Inject constructor(
|
||||
}
|
||||
}
|
||||
.catch { errorEvents.postValue(it) }
|
||||
.throttleLatest(1000)
|
||||
|
||||
val listItems: LiveData<List<OverviewAdapter.Item>> = combine(
|
||||
updateTicker,
|
||||
@@ -128,24 +130,25 @@ class OverviewFragmentVM @Inject constructor(
|
||||
if (permissions.isEmpty() && isBluetoothEnabled) {
|
||||
pods.map {
|
||||
val now = Instant.now()
|
||||
val isMainPod = it.identifier == mainPod?.identifier
|
||||
when (it) {
|
||||
is DualPodDevice -> DualPodsCardVH.Item(
|
||||
now = now,
|
||||
device = it,
|
||||
showDebug = isDebugMode,
|
||||
isMainPod = it == mainPod,
|
||||
isMainPod = isMainPod,
|
||||
)
|
||||
is SinglePodDevice -> SinglePodsCardVH.Item(
|
||||
now = now,
|
||||
device = it,
|
||||
showDebug = isDebugMode,
|
||||
isMainPod = it == mainPod,
|
||||
isMainPod = isMainPod,
|
||||
)
|
||||
else -> UnknownPodDeviceCardVH.Item(
|
||||
now = now,
|
||||
device = it,
|
||||
showDebug = isDebugMode,
|
||||
isMainPod = it == mainPod,
|
||||
isMainPod = isMainPod,
|
||||
)
|
||||
}
|
||||
}.run { items.addAll(this) }
|
||||
|
||||
@@ -31,8 +31,8 @@ class DualPodsCardVH(parent: ViewGroup) :
|
||||
}
|
||||
text = sb
|
||||
|
||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
||||
else setTypeface(typeface, Typeface.NORMAL)
|
||||
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
|
||||
else setTypeface(null, Typeface.NORMAL)
|
||||
|
||||
if (device is DualAirPods && item.showDebug) {
|
||||
append(" [${device.primaryPod.name}]")
|
||||
|
||||
@@ -23,8 +23,8 @@ class SinglePodsCardVH(parent: ViewGroup) :
|
||||
|
||||
name.apply {
|
||||
text = device.getLabel(context)
|
||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
||||
else setTypeface(typeface, Typeface.NORMAL)
|
||||
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
|
||||
else setTypeface(null, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
deviceIcon.setImageResource(device.iconRes)
|
||||
|
||||
+2
-2
@@ -24,8 +24,8 @@ class UnknownPodDeviceCardVH(parent: ViewGroup) :
|
||||
val device = item.device
|
||||
name.apply {
|
||||
text = device.getLabel(context)
|
||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
||||
else setTypeface(typeface, Typeface.NORMAL)
|
||||
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
|
||||
else setTypeface(null, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
lastSeen.text = device.lastSeenFormatted(item.now)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package eu.darken.capod.main.ui.settings.general
|
||||
|
||||
sealed class GeneralSettingsEvents {
|
||||
object SelectDeviceAddressEvent : GeneralSettingsEvents()
|
||||
}
|
||||
+10
-3
@@ -10,7 +10,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||
import eu.darken.capod.common.preferences.PercentSliderPreference
|
||||
import eu.darken.capod.common.uix.PreferenceFragment2
|
||||
import eu.darken.capod.common.uix.PreferenceFragment3
|
||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
@@ -19,9 +19,9 @@ import javax.inject.Inject
|
||||
|
||||
@Keep
|
||||
@AndroidEntryPoint
|
||||
class GeneralSettingsFragment : PreferenceFragment2() {
|
||||
class GeneralSettingsFragment : PreferenceFragment3() {
|
||||
|
||||
private val vm: GeneralSettingsFragmentVM by viewModels()
|
||||
override val vm: GeneralSettingsFragmentVM by viewModels()
|
||||
|
||||
@Inject lateinit var generalSettings: GeneralSettings
|
||||
@Inject lateinit var upgradeRepo: UpgradeRepo
|
||||
@@ -61,6 +61,13 @@ class GeneralSettingsFragment : PreferenceFragment2() {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
vm.events.observe2 {
|
||||
when (it) {
|
||||
GeneralSettingsEvents.SelectDeviceAddressEvent -> mainDeviceAddressPref.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
mainDeviceModelPref.setOnPreferenceClickListener {
|
||||
val dialog = ModelSelectionDialogFactory(requireContext()).create(
|
||||
PodDevice.Model.values().toList(),
|
||||
|
||||
+27
-7
@@ -5,20 +5,40 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.withPrevious
|
||||
import eu.darken.capod.common.livedata.SingleLiveEvent
|
||||
import eu.darken.capod.common.uix.ViewModel3
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class GeneralSettingsFragmentVM @Inject constructor(
|
||||
private val handle: SavedStateHandle,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
@Suppress("unused") private val handle: SavedStateHandle,
|
||||
dispatcherProvider: DispatcherProvider,
|
||||
bluetoothManager: BluetoothManager2,
|
||||
private val generalSettings: GeneralSettings,
|
||||
) : ViewModel3(dispatcherProvider) {
|
||||
|
||||
val bondedDevices = flow {
|
||||
emit(bluetoothManager.bondedDevices().toList())
|
||||
}.asLiveData2()
|
||||
val bondedDevices = bluetoothManager.bondedDevices()
|
||||
.map { it.toList() }
|
||||
.asLiveData2()
|
||||
|
||||
val events = SingleLiveEvent<GeneralSettingsEvents>()
|
||||
|
||||
init {
|
||||
|
||||
generalSettings.monitorMode.flow
|
||||
.withPrevious()
|
||||
.filter { (old, new) ->
|
||||
old != new && new == MonitorMode.AUTOMATIC && generalSettings.mainDeviceAddress.value == null
|
||||
}
|
||||
.onEach { events.postValue(GeneralSettingsEvents.SelectDeviceAddressEvent) }
|
||||
.launchInViewModel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Settings", "General", "VM")
|
||||
|
||||
+3
-3
@@ -9,14 +9,14 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.observe2
|
||||
import eu.darken.capod.common.uix.PreferenceFragment2
|
||||
import eu.darken.capod.common.uix.PreferenceFragment3
|
||||
import javax.inject.Inject
|
||||
|
||||
@Keep
|
||||
@AndroidEntryPoint
|
||||
class DebugSettingsFragment : PreferenceFragment2() {
|
||||
class DebugSettingsFragment : PreferenceFragment3() {
|
||||
|
||||
private val vm: DebugSettingsFragmentVM by viewModels()
|
||||
override val vm: DebugSettingsFragmentVM by viewModels()
|
||||
|
||||
@Inject lateinit var debugSettings: DebugSettings
|
||||
|
||||
|
||||
@@ -10,15 +10,15 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.ClipboardHelper
|
||||
import eu.darken.capod.common.observe2
|
||||
import eu.darken.capod.common.uix.PreferenceFragment2
|
||||
import eu.darken.capod.common.uix.PreferenceFragment3
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import javax.inject.Inject
|
||||
|
||||
@Keep
|
||||
@AndroidEntryPoint
|
||||
class SupportFragment : PreferenceFragment2() {
|
||||
class SupportFragment : PreferenceFragment3() {
|
||||
|
||||
private val vm: SupportFragmentVM by viewModels()
|
||||
override val vm: SupportFragmentVM by viewModels()
|
||||
|
||||
override val preferenceFile: Int = R.xml.preferences_support
|
||||
@Inject lateinit var generalSettings: GeneralSettings
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package eu.darken.capod.main.ui.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Singleton
|
||||
class WidgetManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
|
||||
private val widgetManager by lazy { AppWidgetManager.getInstance(context) }
|
||||
|
||||
private val currentWidgetIds: IntArray
|
||||
get() = widgetManager.getAppWidgetIds(ComponentName(context, PROVIDER_CLASS))
|
||||
|
||||
suspend fun refreshWidgets() {
|
||||
log(TAG) { "refreshWidgets()" }
|
||||
|
||||
log(TAG, VERBOSE) { "Notifying these widget IDs: ${currentWidgetIds.toList()}" }
|
||||
|
||||
val intent = Intent(context, PROVIDER_CLASS).apply {
|
||||
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, currentWidgetIds)
|
||||
}
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val PROVIDER_CLASS = WidgetProvider::class.java
|
||||
val TAG = logTag("Widget", "Manager")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package eu.darken.capod.main.ui.widget
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
import eu.darken.capod.common.upgrade.isPro
|
||||
import eu.darken.capod.main.ui.MainActivity
|
||||
import eu.darken.capod.monitor.core.PodDeviceCache
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.pods.core.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.time.Duration
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WidgetProvider : AppWidgetProvider() {
|
||||
|
||||
@Inject lateinit var podMonitor: PodMonitor
|
||||
@Inject lateinit var podDeviceCache: PodDeviceCache
|
||||
@Inject lateinit var podFactory: PodFactory
|
||||
@Inject lateinit var upgradeRepo: UpgradeRepo
|
||||
@AppScope @Inject lateinit var appScope: CoroutineScope
|
||||
|
||||
private var asyncBarrier: PendingResult? = null
|
||||
|
||||
private fun executeAsync(
|
||||
tag: String,
|
||||
timeout: Duration = Duration.ofSeconds(10),
|
||||
block: suspend () -> Unit
|
||||
) {
|
||||
val start = System.currentTimeMillis()
|
||||
asyncBarrier = goAsync()
|
||||
log(TAG, VERBOSE) { "executeAsync($tag) starting asyncBarrier=$asyncBarrier " }
|
||||
|
||||
appScope.launch {
|
||||
try {
|
||||
withTimeout(timeout.toMillis()) { block() }
|
||||
} catch (e: Exception) {
|
||||
log(TAG, ERROR) { "executeAsync($tag) failed: ${e.asLog()}" }
|
||||
} finally {
|
||||
asyncBarrier?.finish()
|
||||
val stop = System.currentTimeMillis()
|
||||
log(TAG, VERBOSE) { "executeAsync($tag) DONE (${stop - start}ms) " }
|
||||
}
|
||||
}
|
||||
|
||||
log(TAG, VERBOSE) { "executeAsync($block) leaving" }
|
||||
}
|
||||
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
log(TAG) { "onUpdate(appWidgetIds=${appWidgetIds.toList()})" }
|
||||
executeAsync("onUpdate") {
|
||||
appWidgetIds.forEach { appWidgetId ->
|
||||
updateWidget(context, appWidgetManager, appWidgetId, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAppWidgetOptionsChanged(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetId: Int,
|
||||
newOptions: Bundle?
|
||||
) {
|
||||
log(TAG) { "onAppWidgetOptionsChanged(appWidgetId=$appWidgetId, newOptions=$newOptions)" }
|
||||
executeAsync("onAppWidgetOptionsChanged") {
|
||||
updateWidget(context, appWidgetManager, appWidgetId, newOptions)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWidget(
|
||||
context: Context,
|
||||
widgetManager: AppWidgetManager,
|
||||
widgetId: Int,
|
||||
options: Bundle?
|
||||
) {
|
||||
log(TAG) { "updateWidget(widgetId=$widgetId, options=$options)" }
|
||||
|
||||
val device: PodDevice? = podMonitor.latestMainDevice()
|
||||
|
||||
val layout = when {
|
||||
!upgradeRepo.isPro() -> createUpgradeRequiredLayout(context)
|
||||
device is DualPodDevice -> createDualPodLayout(context, device)
|
||||
device is SinglePodDevice -> createSinglePodLayout(context, device)
|
||||
device is PodDevice -> createUnknownPodLayout(context, device)
|
||||
else -> createNoDeviceLayout(context)
|
||||
}
|
||||
widgetManager.updateAppWidget(widgetId, layout)
|
||||
}
|
||||
|
||||
private suspend fun createUpgradeRequiredLayout(
|
||||
context: Context
|
||||
) = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
|
||||
log(TAG, VERBOSE) { "createUpgradeRequiredLayout(context=$context)" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||
|
||||
setTextViewText(R.id.primary, context.getString(R.string.upgrade_capod_label))
|
||||
setTextViewText(R.id.secondary, context.getString(R.string.upgrade_capod_description))
|
||||
setViewVisibility(R.id.secondary, View.VISIBLE)
|
||||
}
|
||||
|
||||
private fun createUnknownPodLayout(
|
||||
context: Context,
|
||||
podDevice: PodDevice,
|
||||
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
|
||||
log(TAG, VERBOSE) { "createUnknownPodLayout(context=$context, podDevice=$podDevice)" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||
|
||||
setTextViewText(R.id.primary, context.getString(R.string.pods_unknown_label))
|
||||
}
|
||||
|
||||
private fun createNoDeviceLayout(
|
||||
context: Context,
|
||||
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
|
||||
log(TAG, VERBOSE) { "createNoDeviceLayout(context=$context)" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||
|
||||
setTextViewText(R.id.primary, context.getString(R.string.overview_nomaindevice_label))
|
||||
}
|
||||
|
||||
private fun createDualPodLayout(
|
||||
context: Context,
|
||||
podDevice: DualPodDevice,
|
||||
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_pod_dual_layout).apply {
|
||||
log(TAG, VERBOSE) { "createSinglePodLayout(context=$context, podDevice=$podDevice)" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||
|
||||
setTextViewText(R.id.headphones_label, podDevice.getLabel(context))
|
||||
|
||||
// Left
|
||||
setTextViewText(R.id.pod_left_label, podDevice.getBatteryLevelLeftPod(context))
|
||||
setViewVisibility(
|
||||
R.id.pod_left_charging,
|
||||
if (podDevice is HasChargeDetectionDual && podDevice.isLeftPodCharging) View.VISIBLE else View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.pod_left_ear,
|
||||
if (podDevice is HasEarDetectionDual && podDevice.isLeftPodInEar) View.VISIBLE else View.GONE
|
||||
)
|
||||
|
||||
// Case
|
||||
setTextViewText(R.id.pod_case_label, (podDevice as? HasCase)?.getBatteryLevelCase(context))
|
||||
setViewVisibility(
|
||||
R.id.pod_case_charging,
|
||||
if (podDevice is HasCase && podDevice.isCaseCharging) View.VISIBLE else View.GONE
|
||||
)
|
||||
|
||||
// Right
|
||||
setTextViewText(R.id.pod_right_label, podDevice.getBatteryLevelRightPod(context))
|
||||
setViewVisibility(
|
||||
R.id.pod_right_charging,
|
||||
if (podDevice is HasChargeDetectionDual && podDevice.isRightPodCharging) View.VISIBLE else View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.pod_right_ear,
|
||||
if (podDevice is HasEarDetectionDual && podDevice.isRightPodInEar) View.VISIBLE else View.GONE
|
||||
)
|
||||
}
|
||||
|
||||
private fun createSinglePodLayout(
|
||||
context: Context,
|
||||
podDevice: SinglePodDevice,
|
||||
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_pod_single_layout).apply {
|
||||
log(TAG, VERBOSE) { "createSinglePodLayout(context=$context, podDevice=$podDevice)" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||
|
||||
setTextViewText(R.id.headphones_label, podDevice.getLabel(context))
|
||||
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(podDevice.batteryHeadsetPercent))
|
||||
setTextViewText(R.id.headphones_battery_label, podDevice.getBatteryLevelHeadset(context))
|
||||
|
||||
setViewVisibility(
|
||||
R.id.headphones_worn,
|
||||
if (podDevice is HasEarDetection && podDevice.isBeingWorn) View.VISIBLE else View.GONE
|
||||
)
|
||||
|
||||
if (this is HasChargeDetectionDual) {
|
||||
setViewVisibility(R.id.headphones_charging, if (isHeadsetBeingCharged) View.VISIBLE else View.GONE)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = logTag("Widget", "Provider")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.darken.capod.monitor.core.worker
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
@@ -17,9 +18,11 @@ import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import eu.darken.capod.common.flow.throttleLatest
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
import eu.darken.capod.main.core.PermissionTool
|
||||
import eu.darken.capod.main.ui.widget.WidgetManager
|
||||
import eu.darken.capod.monitor.core.MonitorComponent
|
||||
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
@@ -48,6 +51,7 @@ class MonitorWorker @AssistedInject constructor(
|
||||
private val autoConnect: AutoConnect,
|
||||
private val popUpReaction: PopUpReaction,
|
||||
private val popUpWindow: PopUpWindow,
|
||||
private val widgetManager: WidgetManager,
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
private val workerScope = MonitorCoroutineScope()
|
||||
@@ -99,6 +103,7 @@ class MonitorWorker @AssistedInject constructor(
|
||||
.setupCommonEventHandlers(TAG) { "PodMonitor" }
|
||||
.onStart { setForeground(monitorNotifications.getForegroundInfo(null)) }
|
||||
.distinctUntilChanged()
|
||||
.throttleLatest(1000)
|
||||
.onEach { currentDevice ->
|
||||
notificationManager.notify(
|
||||
MonitorNotifications.NOTIFICATION_ID,
|
||||
@@ -110,38 +115,49 @@ class MonitorWorker @AssistedInject constructor(
|
||||
}
|
||||
.launchIn(workerScope)
|
||||
|
||||
generalSettings.monitorMode.flow
|
||||
.flatMapLatest { monitorMode ->
|
||||
val missingPermsFlow = permissionTool.missingPermissions.first()
|
||||
permissionTool.missingPermissions
|
||||
.flatMapLatest { missingPermsFlow ->
|
||||
if (missingPermsFlow.isNotEmpty()) {
|
||||
log(TAG, WARN) { "Aborting, permissions are missing for $monitorMode: $missingPermsFlow" }
|
||||
log(TAG, WARN) { "Aborting, permissions are missing: $missingPermsFlow" }
|
||||
workerScope.coroutineContext.cancelChildren()
|
||||
return@flatMapLatest emptyFlow()
|
||||
}
|
||||
|
||||
bluetoothManager.connectedDevices().map { knownDevices ->
|
||||
monitorMode to knownDevices
|
||||
combine(
|
||||
generalSettings.monitorMode.flow,
|
||||
generalSettings.mainDeviceAddress.flow,
|
||||
bluetoothManager.connectedDevices(),
|
||||
) { monitorMode, mainAddress, connectedDevices ->
|
||||
listOf(monitorMode, mainAddress, connectedDevices)
|
||||
}
|
||||
}
|
||||
.setupCommonEventHandlers(TAG) { "MonitorMode" }
|
||||
.flatMapLatest { (monitorMode, devices) ->
|
||||
.flatMapLatest { arguments ->
|
||||
val monitorMode = arguments[0] as MonitorMode
|
||||
val mainAddress = arguments[1] as String?
|
||||
val devices = arguments[2] as Collection<BluetoothDevice>
|
||||
|
||||
log(TAG) { "Monitor mode: $monitorMode" }
|
||||
when (monitorMode) {
|
||||
MonitorMode.MANUAL -> flow<Unit> {
|
||||
// Cancel worker, ui scans manually
|
||||
workerScope.coroutineContext.cancelChildren()
|
||||
}
|
||||
MonitorMode.MANUAL -> flow<Unit> {
|
||||
// Cancel worker, ui scans manually
|
||||
workerScope.coroutineContext.cancelChildren()
|
||||
}
|
||||
MonitorMode.ALWAYS -> emptyFlow()
|
||||
MonitorMode.AUTOMATIC -> flow {
|
||||
val mainAddress = generalSettings.mainDeviceAddress.value
|
||||
if (devices.any { it.address == mainAddress }) {
|
||||
log(TAG) { "MainDevice is connected ($mainAddress), aborting any timeout." }
|
||||
} else {
|
||||
log(TAG) { "No Pods are connected, canceling worker soon." }
|
||||
delay(15 * 1000)
|
||||
log(TAG) { "Canceling worker now, still no Pods connected." }
|
||||
when {
|
||||
mainAddress == null && devices.isNotEmpty() -> {
|
||||
log(TAG, WARN) { "Main device address not set, staying alive while any is connected" }
|
||||
}
|
||||
devices.any { it.address == mainAddress } -> {
|
||||
log(TAG) { "MainDevice is connected ($mainAddress), aborting any timeout." }
|
||||
}
|
||||
else -> {
|
||||
log(TAG) { "No known Pods are connected, canceling worker soon." }
|
||||
delay(15 * 1000)
|
||||
log(TAG) { "Canceling worker now, still no Pods connected." }
|
||||
|
||||
workerScope.coroutineContext.cancelChildren()
|
||||
workerScope.coroutineContext.cancelChildren()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.darken.capod.reaction.ui
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.Keep
|
||||
@@ -10,7 +11,7 @@ import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.uix.PreferenceFragment2
|
||||
import eu.darken.capod.common.uix.PreferenceFragment3
|
||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
@@ -21,9 +22,9 @@ import javax.inject.Inject
|
||||
|
||||
@Keep
|
||||
@AndroidEntryPoint
|
||||
class ReactionSettingsFragment : PreferenceFragment2() {
|
||||
class ReactionSettingsFragment : PreferenceFragment3() {
|
||||
|
||||
private val vm: ReactionSettingsFragmentVM by viewModels()
|
||||
override val vm: ReactionSettingsFragmentVM by viewModels()
|
||||
|
||||
@Inject lateinit var generalSettings: GeneralSettings
|
||||
@Inject lateinit var reactionSettings: ReactionSettings
|
||||
@@ -35,6 +36,7 @@ class ReactionSettingsFragment : PreferenceFragment2() {
|
||||
override val preferenceFile: Int = R.xml.preferences_reactions
|
||||
|
||||
private var isPro: Boolean = false
|
||||
private var bondedDevices: List<BluetoothDevice> = emptyList()
|
||||
private val autoConnectConditionPref by lazy { findPreference<ListPreference>(settings.autoConnectCondition.key)!! }
|
||||
|
||||
override fun onPreferencesCreated() {
|
||||
@@ -66,10 +68,9 @@ class ReactionSettingsFragment : PreferenceFragment2() {
|
||||
preference.isChecked = false
|
||||
return true
|
||||
} else if (generalSettings.mainDeviceAddress.value == null) {
|
||||
val devices = vm.bondedDevices
|
||||
DeviceSelectionDialogFactory(requireContext()).create(
|
||||
devices = devices,
|
||||
current = devices.firstOrNull { it.address == generalSettings.mainDeviceAddress.value }
|
||||
devices = bondedDevices,
|
||||
current = bondedDevices.firstOrNull { it.address == generalSettings.mainDeviceAddress.value }
|
||||
) { selected ->
|
||||
generalSettings.mainDeviceAddress.value = selected?.address
|
||||
if (selected != null) preference.isChecked = true
|
||||
@@ -87,14 +88,21 @@ class ReactionSettingsFragment : PreferenceFragment2() {
|
||||
return super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
|
||||
// Some UI interactions shortly turn autoConnect on, then off again
|
||||
private val previousMonitorMode by lazy { generalSettings.monitorMode.value }
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
settings.autoConnect.flow.asLiveData().observe2 {
|
||||
generalSettings.monitorMode.value = MonitorMode.ALWAYS
|
||||
autoConnectConditionPref.isEnabled = it
|
||||
settings.autoConnect.flow.asLiveData().observe2 { isEnabled ->
|
||||
if (isEnabled) generalSettings.monitorMode.value = MonitorMode.ALWAYS
|
||||
else generalSettings.monitorMode.value = previousMonitorMode
|
||||
|
||||
autoConnectConditionPref.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
vm.isPro.observe2 { isPro = it }
|
||||
|
||||
vm.bondedDevices.observe2 { bondedDevices = it }
|
||||
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,12 @@ class ReactionSettingsFragmentVM @Inject constructor(
|
||||
private val upgradeRepo: UpgradeRepo,
|
||||
) : ViewModel3(dispatcherProvider) {
|
||||
|
||||
val bondedDevices = bluetoothManager.bondedDevices().toList()
|
||||
|
||||
val isPro = upgradeRepo.upgradeInfo.map { it.isPro }.asLiveData2()
|
||||
|
||||
val bondedDevices = bluetoothManager.bondedDevices()
|
||||
.map { it.toList() }
|
||||
.asLiveData2()
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Settings", "Reaction", "VM")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="28.575"
|
||||
android:viewportHeight="28.575">
|
||||
<path
|
||||
android:pathData="M14.288,4.762A9.525,9.525 0,0 0,7.327 7.787A6.91,6.91 0,0 0,14.101 13.357A6.91,6.91 0,0 0,20.934 7.472A9.525,9.525 0,0 0,14.288 4.762zM11.876,7.789A1.08,1.08 0,0 1,12.956 8.869A1.08,1.08 0,0 1,11.876 9.949A1.08,1.08 0,0 1,10.797 8.869A1.08,1.08 0,0 1,11.876 7.789zM16.301,7.789A1.08,1.08 0,0 1,17.381 8.869A1.08,1.08 0,0 1,16.301 9.949A1.08,1.08 0,0 1,15.221 8.869A1.08,1.08 0,0 1,16.301 7.789z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.147417"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M11.33,12.714L10.689,13.825A1.038,1.038 75,0 1,9.271 14.205L9.271,14.205A1.038,1.038 75,0 1,8.891 12.787L9.533,11.676A1.038,1.038 75,0 1,10.95 11.296L10.95,11.296A1.038,1.038 75,0 1,11.33 12.714z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.119"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M16.833,12.714L17.475,13.825A1.038,1.038 105,0 0,18.893 14.205L18.893,14.205A1.038,1.038 105,0 0,19.273 12.787L18.631,11.676A1.038,1.038 105,0 0,17.213 11.296L17.213,11.296A1.038,1.038 105,0 0,16.833 12.714z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.119"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M11.331,15.805C10.562,15.802 9.752,16.161 9.018,16.825L9.018,19.202C9.921,20.377 10.92,20.352 11.931,20.192L11.931,23.51A9.525,9.525 0,0 0,13.607 23.787L13.607,17.279C13.018,16.272 12.202,15.807 11.331,15.805zM10.253,17.712A0.306,0.306 0,0 1,10.559 18.017A0.306,0.306 0,0 1,10.253 18.323A0.306,0.306 0,0 1,9.948 18.017A0.306,0.306 0,0 1,10.253 17.712z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.10993"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M16.915,15.805C16.044,15.807 15.228,16.272 14.638,17.279L14.638,23.805A9.525,9.525 0,0 0,16.315 23.593L16.315,20.192C17.326,20.352 18.325,20.377 19.228,19.202L19.228,16.825C18.494,16.161 17.684,15.802 16.915,15.805zM18.031,17.712A0.306,0.306 0,0 1,18.337 18.017A0.306,0.306 0,0 1,18.031 18.323A0.306,0.306 0,0 1,17.725 18.017A0.306,0.306 0,0 1,18.031 17.712z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.10993"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Notification.Container"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
style="@style/Notification.Container"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/Notification.Container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="@style/PodWidget.Container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/widget_root"
|
||||
style="@style/PodWidget.Container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/primary"
|
||||
style="@style/PodWidget.TextPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:text="@string/pods_unknown_label"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/secondary"
|
||||
style="@style/PodWidget.TextSecondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLines="4"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
tools:text="@string/pods_unknown_label"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/PodWidget.Container"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headphones_label"
|
||||
style="@style/PodWidget.TextPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:textSize="12sp"
|
||||
tools:text="AirPods Max" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_icon"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_airpod_left_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_left_label"
|
||||
style="@style/PodInfoItemText.Notification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
tools:text="100%" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_charging"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_baseline_power_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_ear"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_baseline_hearing_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_icon"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_airpod_right_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_right_label"
|
||||
style="@style/PodInfoItemText.Notification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
tools:text="100%" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_charging"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_baseline_power_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_ear"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_baseline_hearing_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_case_icon"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_airpod_case_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_case_label"
|
||||
style="@style/PodInfoItemText.Notification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
tools:text="100%" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_case_charging"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_baseline_power_24" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/PodWidget.Container"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headphones_label"
|
||||
style="@style/PodWidget.TextPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
tools:text="AirPods Max" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/headphones_battery_icon"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_baseline_battery_unknown_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headphones_battery_label"
|
||||
style="@style/PodInfoItemText.Notification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
tools:text="100%" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/headphones_charging"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_baseline_power_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/headphones_worn"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_baseline_hearing_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/PodWidget.Container"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_icon"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_airpod_left_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_left_label"
|
||||
style="@style/PodInfoItemText.Notification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="100%"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_charging"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_baseline_power_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_left_ear"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_baseline_hearing_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_icon"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_airpod_right_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_right_label"
|
||||
style="@style/PodInfoItemText.Notification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="100%"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_charging"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_baseline_power_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_right_ear"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_baseline_hearing_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_case_icon"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_airpod_case_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pod_case_label"
|
||||
style="@style/PodInfoItemText.Notification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="100%"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pod_case_charging"
|
||||
style="@style/PodInfoItemIcon.Notification"
|
||||
android:src="@drawable/ic_baseline_power_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
@@ -53,6 +53,7 @@
|
||||
<string name="settings_support_description">Si vous avez besoin d’aide.</string>
|
||||
<string name="issue_tracker_label">Gestionnaire de problèmes</string>
|
||||
<string name="issue_tracker_description">Un gestionnaire public de problèmes pour les signalements de bogues et les demandes de fonction (anglais seulement).</string>
|
||||
<string name="discord_label">Discord</string>
|
||||
<string name="discord_description">Un endroit à fréquenter où poser des questions.</string>
|
||||
<string name="changelog_label">Journal des changements</string>
|
||||
<string name="settings_label">Paramètres</string>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="general_upgrade_action">Tingkatkan</string>
|
||||
<string name="general_check_action">Memeriksa</string>
|
||||
<string name="general_close_action">Tutup</string>
|
||||
<string name="upgrade_capod_label">Perbaharui CAPod</string>
|
||||
<string name="upgrade_capod_description">Dapatkan fitur tambahan & bantu dukung pengembang.</string>
|
||||
<string name="settings_monitor_mode_label">Mode monitor</string>
|
||||
<string name="settings_monitor_mode_description">Dalam keadaan apa aplikasi ini memonitor data Bluetooth.</string>
|
||||
@@ -52,6 +53,7 @@
|
||||
<string name="settings_support_description">Jika anda membutuhkan bantuan.</string>
|
||||
<string name="issue_tracker_label">Pelacak masalah</string>
|
||||
<string name="issue_tracker_description">Pelacak masalah publik untuk laporan bug & permintaan fitur (hanya dalam bahasa Inggris).</string>
|
||||
<string name="discord_label">Discord</string>
|
||||
<string name="discord_description">Tempat nongkrong & bertanya-tanya.</string>
|
||||
<string name="changelog_label">Catatan perubahan</string>
|
||||
<string name="settings_label">Pengaturan</string>
|
||||
@@ -73,4 +75,5 @@
|
||||
<string name="help_translate_label">Terjemahan</string>
|
||||
<string name="help_translate_description">Bantu terjemahkan applikasi ini ke bahasa favorit anda.</string>
|
||||
<string name="translators_thanks_title">Penerjemah</string>
|
||||
<string name="translators_thanks_description">gelapkan</string>
|
||||
</resources>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<string name="general_close_action">Chiudi</string>
|
||||
<string name="upgrade_capod_label">Aggiorna CAPod</string>
|
||||
<string name="upgrade_capod_description">Ottieni funzionalità aggiuntive e supporta lo sviluppatore.</string>
|
||||
<string name="settings_monitor_mode_label">Modalità monitor</string>
|
||||
<string name="settings_monitor_mode_description">Sotto quali circostanze questa applicazione monitora i dati Bluetooth.</string>
|
||||
<string name="settings_scanner_mode_label">Modalità di scansione</string>
|
||||
<string name="settings_scanner_mode_description">Il bluetooth a basso consumo energetico dovrebbe dare priorità alle performance o al risparmio della batteria?</string>
|
||||
@@ -50,6 +51,7 @@
|
||||
<string name="settings_support_installid_desc">Le segnalazioni automatiche degli errori sono anonime. Condividi il tuo ID di installazione con lo sviluppatore se necessità di trovare la tua segnalazione all\'interno del registro segnalazioni.</string>
|
||||
<string name="settings_support_label">Supporto</string>
|
||||
<string name="settings_support_description">Se ti serve un aiuto.</string>
|
||||
<string name="issue_tracker_label">Tracker problemi</string>
|
||||
<string name="issue_tracker_description">Un issue tracker pubblico per segnalare i problemi e le richieste per aggiungere nuove funzionalità (solo in inglese).</string>
|
||||
<string name="discord_label">Discord</string>
|
||||
<string name="discord_description">Un posto dove poter fare domande.</string>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<string name="changelog_label">更新の内容</string>
|
||||
<string name="settings_label">設定</string>
|
||||
<string name="settings_privacy_policy_label">個人情報の取り扱いについて</string>
|
||||
<string name="settings_privacy_policy_desc">責任を持ってデータを取り扱います。</string>
|
||||
<string name="settings_privacy_policy_desc">責任を持ってデータを取り扱います</string>
|
||||
<string name="settings_licenses_label">ライセンス情報</string>
|
||||
<string name="settings_category_other_label">その他</string>
|
||||
<string name="settings_general_label">全般</string>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="widget_root" type="id" />
|
||||
</resources>
|
||||
@@ -89,4 +89,6 @@
|
||||
|
||||
<string name="translators_thanks_title">Translators</string>
|
||||
<string name="translators_thanks_description">darken</string>
|
||||
|
||||
<string name="widget_description">A widget showing the last known device status.</string>
|
||||
</resources>
|
||||
@@ -7,14 +7,26 @@
|
||||
<item name="android:layout_marginEnd">8dp</item>
|
||||
</style>
|
||||
|
||||
<style name="Notification">
|
||||
|
||||
</style>
|
||||
|
||||
<style name="Notification.Container">
|
||||
<!-- <item name="android:background">?android:attr/colorBackground</item>-->
|
||||
<item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item>
|
||||
</style>
|
||||
|
||||
<style name="PodInfoItemIcon.Notification" parent="TextAppearance.Compat.Notification.Title">
|
||||
<item name="android:layout_height">20dp</item>
|
||||
<item name="android:layout_width">20dp</item>
|
||||
<item name="android:tint">?android:attr/colorAccent</item>
|
||||
<item name="tint">?android:attr/colorAccent</item>
|
||||
</style>
|
||||
|
||||
<style name="PodInfoItemText.Notification" parent="TextAppearance.Compat.Notification.Title">
|
||||
<item name="android:singleLine">true</item>
|
||||
<item name="android:ellipsize">end</item>
|
||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||
</style>
|
||||
|
||||
<style name="PodInfoItemIcon" parent="TextAppearance.MaterialComponents.Body2">
|
||||
@@ -26,4 +38,29 @@
|
||||
<item name="android:singleLine">true</item>
|
||||
<item name="android:ellipsize">end</item>
|
||||
</style>
|
||||
|
||||
<style name="PodWidget">
|
||||
|
||||
</style>
|
||||
|
||||
<style name="PodWidget.Container">
|
||||
<item name="android:background">?android:attr/colorBackground</item>
|
||||
<item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item>
|
||||
<item name="android:paddingStart">16dp</item>
|
||||
<item name="android:paddingEnd">16dp</item>
|
||||
<item name="android:paddingTop">8dp</item>
|
||||
<item name="android:paddingBottom">8dp</item>
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:id">@id/widget_root</item>
|
||||
<item name="android:gravity">center</item>
|
||||
</style>
|
||||
|
||||
<style name="PodWidget.TextPrimary">
|
||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||
</style>
|
||||
|
||||
<style name="PodWidget.TextSecondary">
|
||||
<item name="android:textColor">?android:attr/textColorSecondary</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:minWidth="80dp"
|
||||
android:minHeight="40dp"
|
||||
android:targetCellWidth="2"
|
||||
android:targetCellHeight="1"
|
||||
android:updatePeriodMillis="86400000"
|
||||
android:description="@string/widget_description"
|
||||
android:previewLayout="@layout/widget_preview_layout"
|
||||
android:initialLayout="@layout/widget_loading_layout"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:widgetFeatures="reconfigurable|configuration_optional"
|
||||
tools:targetApi="s" />
|
||||
@@ -14,13 +14,16 @@ object ProjectConfig {
|
||||
const val targetSdk = 33
|
||||
|
||||
object Version {
|
||||
const val major = 2
|
||||
const val minor = 2
|
||||
const val patch = 0
|
||||
const val build = 0
|
||||
val versionProperties = Properties().apply {
|
||||
load(FileInputStream(File("version.properties")))
|
||||
}
|
||||
val major = versionProperties.getProperty("project.versioning.major").toInt()
|
||||
val minor = versionProperties.getProperty("project.versioning.minor").toInt()
|
||||
val patch = versionProperties.getProperty("project.versioning.patch").toInt()
|
||||
val build = versionProperties.getProperty("project.versioning.build").toInt()
|
||||
|
||||
const val name = "${major}.${minor}.${patch}-rc${build}"
|
||||
const val code = major * 10000000 + minor * 100000 + patch * 1000 + build * 10
|
||||
val name = "${major}.${minor}.${patch}-rc${build}"
|
||||
val code = major * 10000000 + minor * 100000 + patch * 1000 + build * 10
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
Bugfixes and performance improvements.
|
||||
¯\_(ツ)_/¯
|
||||
|
||||
Now with more widgets!
|
||||
But also bugfixes and performance improvements.
|
||||
¯\_(ツ)_/¯
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
@@ -1,18 +1,18 @@
|
||||
CAPod is a companion app for AirPods.
|
||||
CAPod es una aplicación complementaria para AirPods.
|
||||
|
||||
Features:
|
||||
Funciones:
|
||||
|
||||
* Battery level for pods and cases.
|
||||
* Charging status for pods and case.
|
||||
* Additional infos about connection, microphone and case.
|
||||
* Can receive and show all nearby devices.
|
||||
* Ear detection with automatic play/pause.
|
||||
* Automatically connect phone and AirPods.
|
||||
* Show popup when case is opened.
|
||||
* Nivel de batería para los auriculares y estuches.
|
||||
* Indicador de carga de auriculares y estuche.
|
||||
* Información adicional de conexión, micrófono y estuche.
|
||||
* Puede recibir y mostrar todos los dispositivos cercanos.
|
||||
* Detección de oído con reproducción/pausa automática.
|
||||
* Conecta automáticamente el teléfono y los AirPods.
|
||||
* Muestra un aviso emergente de estuche abierto.
|
||||
|
||||
CAPod is ad-free. Some features require an in-app purchase.
|
||||
CAPod es libre de publicidad. Algunas funciones requieren compras en la aplicación.
|
||||
|
||||
Most popular AirPods and Beats devices are supported.
|
||||
If your device is similar to AirPods but not yet supported, send me a short mail.
|
||||
Válido para la mayoría de AirPods populares y Beats.
|
||||
Si su dispositivo es similar a AirPods pero aún no es compatible, envíeme un correo.
|
||||
|
||||
Got a cool idea for a new feature? Reach out!
|
||||
¿Tienes una buena idea para una nueva característica? ¡Háznoslo saber!
|
||||
@@ -1 +1 @@
|
||||
CAPod is a companion app for AirPods on Android.
|
||||
CAPod es una aplicación complementaria para AirPods en Android.
|
||||
@@ -1 +1 @@
|
||||
CAPod - Companion for AirPods
|
||||
CAPod - Asistente para AirPods
|
||||
Executable
+458
@@ -0,0 +1,458 @@
|
||||
#!/bin/bash
|
||||
# Based on
|
||||
# * https://gist.github.com/jv-k/703e79306554c26a65a7cfdb9ca119c6
|
||||
# * https://github.com/jv-k/ver-bump
|
||||
|
||||
# █▄▄ █░█ █▀▄▀█ █▀█ ▄▄ █░█ █▀▀ █▀█ █▀ █ █▀█ █▄░█
|
||||
# █▄█ █▄█ █░▀░█ █▀▀ ░░ ▀▄▀ ██▄ █▀▄ ▄█ █ █▄█ █░▀█
|
||||
#
|
||||
#
|
||||
# Description:
|
||||
# - This script automates bumping the git software project's version using automation.
|
||||
|
||||
# - It does several things that are typically required for releasing a Git repository, like git tagging,
|
||||
# automatic updating of CHANGELOG.md, and incrementing the version number in various JSON files.
|
||||
|
||||
# - Increments / suggests the current software project's version number
|
||||
# - Adds a Git tag, named after the chosen version number
|
||||
# - Updates CHANGELOG.md
|
||||
# - Updates VERSION file
|
||||
# - Commits files to a new branch
|
||||
# - Pushes to remote (optionally)
|
||||
# - Updates "version" : "x.x.x" tag in JSON files if [-v file1 -v file2...] argument is supplied.
|
||||
#
|
||||
# Usage:
|
||||
# ./bump-version.sh [-v <version number>] [-m <release message>] [-j <file1>] [-j <file2>].. [-n] [-p] [-b] [-h]
|
||||
#
|
||||
# Options:
|
||||
# -v <version number> Specify a manual version number
|
||||
# -m <release message> Custom release message.
|
||||
# -f <filename.json> Update version number inside JSON files.
|
||||
# * For multiple files, add a separate -f option for each one,
|
||||
# * For example: ./bump-version.sh -f src/plugin/package.json -f composer.json
|
||||
# -p <repository alias> Push commits to remote repository, eg `-p origin`
|
||||
# -n Don't perform a commit automatically.
|
||||
# * You may want to do that yourself, for example.
|
||||
# -b Don't create automatic `release-<version>` branch
|
||||
# -h Show help message.
|
||||
|
||||
#
|
||||
# Detailed notes:
|
||||
# – The contents of the `VERSION` file which should be a semantic version number such as "1.2.3"
|
||||
# or even "1.2.3-beta+001.ab"
|
||||
#
|
||||
# – It pulls a list of changes from git history & prepends to a file called CHANGELOG.md
|
||||
# under the title of the new version # number, allows the user to review and update the changelist
|
||||
#
|
||||
# – Creates a Git tag with the version number
|
||||
#
|
||||
# - Creates automatic `release-<version>` branch
|
||||
#
|
||||
# – Commits the new version to the current repository
|
||||
#
|
||||
# – Optionally pushes the commit to remote repository
|
||||
#
|
||||
# – Make sure to set execute permissions for the script, eg `$ chmod 755 bump-version.sh`
|
||||
#
|
||||
# Credits:
|
||||
# – https://github.com/jv-k/bump-version
|
||||
#
|
||||
# - Inspired by the scripts from @pete-otaqui and @mareksuscak
|
||||
# https://gist.github.com/pete-otaqui/4188238
|
||||
# https://gist.github.com/mareksuscak/1f206fbc3bb9d97dec9c
|
||||
#
|
||||
|
||||
NOW="$(date +'%B %d, %Y')"
|
||||
|
||||
# ANSI/VT100 colours
|
||||
YELLOW='\033[1;33m'
|
||||
LIGHTYELLOW='\033[0;33m'
|
||||
RED='\033[0;31m'
|
||||
LIGHTRED='\033[1;31m'
|
||||
GREEN='\033[0;32m'
|
||||
LIGHTGREEN='\033[1;32m'
|
||||
BLUE='\033[0;34m'
|
||||
LIGHTBLUE='\033[1;34m'
|
||||
PURPLE='\033[0;35m'
|
||||
LIGHTPURPLE='\033[1;35m'
|
||||
CYAN='\033[0;36m'
|
||||
LIGHTCYAN='\033[1;36m'
|
||||
WHITE='\033[1;37m'
|
||||
LIGHTGRAY='\033[0;37m'
|
||||
DARKGRAY='\033[1;30m'
|
||||
BOLD="\033[1m"
|
||||
INVERT="\033[7m"
|
||||
RESET='\033[0m'
|
||||
|
||||
# Default options
|
||||
FLAG_JSON="false"
|
||||
FLAG_PUSH="false"
|
||||
|
||||
I_OK="✅"
|
||||
I_STOP="🚫"
|
||||
I_ERROR="❌"
|
||||
I_END="👋🏻"
|
||||
|
||||
S_NORM="${WHITE}"
|
||||
S_LIGHT="${LIGHTGRAY}"
|
||||
S_NOTICE="${GREEN}"
|
||||
S_QUESTION="${YELLOW}"
|
||||
S_WARN="${LIGHTRED}"
|
||||
S_ERROR="${RED}"
|
||||
|
||||
V_SUGGEST="0.1.2-rc5" # This is suggested in case VERSION file or user supplied version via -v is missing
|
||||
|
||||
V_MAJOR="" # 0
|
||||
V_MINOR="" # 1
|
||||
V_PATCH="" # 2
|
||||
V_BUILD_TYPE="" # rc
|
||||
V_BUILD_COUNTER="" # 5
|
||||
V_NAME=""
|
||||
V_CODE=""
|
||||
|
||||
SCRIPT_VER="1.0"
|
||||
|
||||
GIT_MSG="Release: "
|
||||
REL_NOTE=""
|
||||
REL_PREFIX="release/"
|
||||
PUSH_DEST="origin"
|
||||
|
||||
# Show credits & help
|
||||
usage() {
|
||||
echo -e "$GREEN" \
|
||||
"\n █▄▄ █░█ █▀▄▀█ █▀█ ▄▄ █░█ █▀▀ █▀█ █▀ █ █▀█ █▄░█ " \
|
||||
"\n █▄█ █▄█ █░▀░█ █▀▀ ░░ ▀▄▀ ██▄ █▀▄ ▄█ █ █▄█ █░▀█ " \
|
||||
"\n\t\t\t\t\t$LIGHTGRAY v${SCRIPT_VER}"
|
||||
|
||||
echo -e " ${S_NORM}${BOLD}Usage:${RESET}" \
|
||||
"\n $0 [-v <version number>] [-m <release message>] [-n] [-p] [-h]" 1>&2
|
||||
|
||||
echo -e "\n ${S_NORM}${BOLD}Options:${RESET}"
|
||||
echo -e " $S_WARN-v$S_NORM <version number>\tSpecify a manual version number"
|
||||
echo -e " $S_WARN-m$S_NORM <release message>\tCustom release message."
|
||||
echo -e " $S_WARN-p$S_NORM \t\t\tPush commits to ORIGIN. "
|
||||
echo -e " $S_WARN-n$S_NORM \t\t\tDon't perform a commit automatically. " \
|
||||
"\n\t\t\t* You may want to do that manually after checking everything, for example."
|
||||
echo -e " $S_WARN-b$S_NORM \t\t\tDon't create automatic \`release-<version>\` branch"
|
||||
echo -e " $S_WARN-h$S_NORM \t\t\tShow this help message. "
|
||||
echo -e "\n ${S_NORM}${BOLD}Original author: $S_LIGHT https://github.com/jv-t/bump-version $RESET"
|
||||
echo -e "\n ${S_NORM}${BOLD}Changes by: $S_LIGHT https://github.com/d4rken $RESET\n"
|
||||
}
|
||||
|
||||
# If there are no commits in repo, quit, because you can't tag with zero commits.
|
||||
check-commits-exist() {
|
||||
git rev-parse HEAD &>/dev/null
|
||||
if [ ! "$?" -eq 0 ]; then
|
||||
echo -e "\n${I_STOP} ${S_ERROR}Your current branch doesn't have any commits yet. Can't tag without at least one commit." >&2
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
exit_abnormal() {
|
||||
echo -e " ${S_LIGHT}––––––"
|
||||
usage # Show help
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Process script options
|
||||
process-arguments() {
|
||||
local OPTIONS OPTIND OPTARG
|
||||
|
||||
# Get positional parameters
|
||||
JSON_FILES=()
|
||||
while getopts ":v:p:m:hbn" OPTIONS; do # Note: Adding the first : before the flags takes control of flags and prevents default error msgs.
|
||||
case "$OPTIONS" in
|
||||
h)
|
||||
# Show help
|
||||
exit_abnormal
|
||||
;;
|
||||
v)
|
||||
# User has supplied a version number
|
||||
V_USR_SUPPLIED=$OPTARG
|
||||
;;
|
||||
m)
|
||||
REL_NOTE=$OPTARG
|
||||
# Custom release note
|
||||
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Release note:" ${S_NORM}"'"$REL_NOTE"'"
|
||||
;;
|
||||
p)
|
||||
FLAG_PUSH=true
|
||||
PUSH_DEST=${OPTARG} # Replace default with user input
|
||||
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Pushing to <${S_NORM}${PUSH_DEST}${S_LIGHT}>, as the last action in this script."
|
||||
;;
|
||||
n)
|
||||
FLAG_NOCOMMIT=true
|
||||
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Disable commit after tagging."
|
||||
;;
|
||||
b)
|
||||
FLAG_NOBRANCH=true
|
||||
echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Disable committing to new branch."
|
||||
;;
|
||||
\?)
|
||||
echo -e "\n${I_ERROR}${S_ERROR} Invalid option: ${S_WARN}-$OPTARG" >&2
|
||||
echo
|
||||
exit_abnormal
|
||||
;;
|
||||
:)
|
||||
echo -e "\n${I_ERROR}${S_ERROR} Option ${S_WARN}-$OPTARG ${S_ERROR}requires an argument." >&2
|
||||
echo
|
||||
exit_abnormal
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Suggests version from VERSION file, or grabs from user supplied -v <version>.
|
||||
# If none is set, suggest default from options.
|
||||
process-version() {
|
||||
V_RAW=""
|
||||
|
||||
V_FILE_REGEX='^([0-9]+\.[0-9]+\.[0-9]+-[a-zA-Z]+[0-9]+) ([0-9]+)$'
|
||||
V_FILE_RAW="$(cat VERSION)"
|
||||
if [ -f VERSION ] && [ -s VERSION ] && [[ $V_FILE_RAW =~ $V_FILE_REGEX ]]; then
|
||||
V_PREV="${BASH_REMATCH[1]}"
|
||||
V_SUGGEST=$V_PREV
|
||||
echo -e "\n${S_NOTICE}Current version from <${S_NORM}VERSION${S_NOTICE}> file: ${S_NORM}$V_PREV"
|
||||
else
|
||||
echo -ne "\n${S_WARN}The [${S_NORM}VERSION${S_WARN}] "
|
||||
if [ ! -f VERSION ]; then
|
||||
echo "VERSION file was not found."
|
||||
elif [ ! -s VERSION ]; then
|
||||
echo "VERSION file is empty."
|
||||
else
|
||||
echo "could not be parsed."
|
||||
fi
|
||||
fi
|
||||
|
||||
# If a version number is supplied by the user with [-v <version number>], then use it
|
||||
if [ -n "$V_USR_SUPPLIED" ]; then
|
||||
echo -e "\n${S_NOTICE}You selected version using [-v]:" "${S_WARN}${V_USR_SUPPLIED}"
|
||||
V_RAW="${V_USR_SUPPLIED}"
|
||||
else
|
||||
echo -ne "\n${S_QUESTION}Enter a new version number [${S_NORM}$V_SUGGEST${S_QUESTION}]: "
|
||||
echo -ne "$S_WARN"
|
||||
read -r V_RAW
|
||||
fi
|
||||
|
||||
if [ -z "$V_RAW" ]; then
|
||||
V_RAW=$V_PREV
|
||||
fi
|
||||
|
||||
if [ -z "$V_RAW" ]; then
|
||||
echo -e "\n${I_STOP} ${S_ERROR}Error: No version was supplied (no file, no CLI)\n"
|
||||
exit_abnormal
|
||||
fi
|
||||
|
||||
SEMVER_REGEX='^([0-9]+)\.([0-9]+)\.([0-9]+)-([a-zA-Z]+)([0-9]+)$'
|
||||
|
||||
echo -e "\n${S_NOTICE}Parsing ${V_RAW}"
|
||||
|
||||
if [[ $V_RAW =~ $SEMVER_REGEX ]]; then
|
||||
echo -e "\n${I_OK} ${S_NOTICE} Successfully parsed ${V_RAW} to ${BASH_REMATCH[0]}"
|
||||
V_MAJOR="${BASH_REMATCH[1]}"
|
||||
echo "V_MAJOR=$V_MAJOR"
|
||||
V_MINOR="${BASH_REMATCH[2]}"
|
||||
echo "V_MINOR=$V_MINOR"
|
||||
V_PATCH="${BASH_REMATCH[3]}"
|
||||
echo "V_PATCH=$V_PATCH"
|
||||
V_BUILD_TYPE="${BASH_REMATCH[4]}"
|
||||
echo "V_BUILD_TYPE=$V_BUILD_TYPE"
|
||||
V_BUILD_COUNTER="${BASH_REMATCH[5]}"
|
||||
echo "V_BUILD_COUNTER=$V_BUILD_COUNTER"
|
||||
else
|
||||
echo -e "\n${I_STOP} ${S_ERROR}Error: Failed to parse $V_RAW\n"
|
||||
exit_abnormal
|
||||
fi
|
||||
|
||||
# If no version was provided, bump the previous version
|
||||
if [ -z "$V_USR_SUPPLIED" ]; then
|
||||
if [ "$V_BUILD_COUNTER" -eq "$V_BUILD_COUNTER" ] 2>/dev/null; then # discard stderr (2) output to black hole (suppress it)
|
||||
V_BUILD_COUNTER=$((V_BUILD_COUNTER + 1)) # Increment
|
||||
fi
|
||||
fi
|
||||
|
||||
V_NAME="$V_MAJOR.$V_MINOR.$V_PATCH-$V_BUILD_TYPE$V_BUILD_COUNTER"
|
||||
V_CODE=$((V_MAJOR * 10000000 + V_MINOR * 100000 + V_PATCH * 1000 + V_BUILD_COUNTER * 10))
|
||||
echo -e "${S_NOTICE}Setting version to [${S_NORM}${V_NAME} (${V_CODE})${S_NOTICE}] ...."
|
||||
}
|
||||
|
||||
# Only tag if tag doesn't already exist
|
||||
check-tag-exists() {
|
||||
TAG_CHECK_EXISTS=$(git tag -l v"$V_NAME")
|
||||
if [ -n "$TAG_CHECK_EXISTS" ]; then
|
||||
echo -e "\n${I_STOP} ${S_ERROR}Error: A release with that tag version number already exists!\n"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
# $1 : version
|
||||
# $2 : release note
|
||||
create-tag() {
|
||||
if [ -z "$2" ]; then
|
||||
# Default release note
|
||||
git tag -a "v$1" -m "Tag version $1."
|
||||
else
|
||||
# Custom release note
|
||||
git tag -a "v$1" -m "$2"
|
||||
fi
|
||||
echo -e "\n${I_OK} ${S_NOTICE}Added GIT tag"
|
||||
}
|
||||
|
||||
# Update version.properties which is used by Gradle to generate the `versionName` and `versionCode`
|
||||
do-version-properties() {
|
||||
PROPS_FILE_NAME="version.properties"
|
||||
echo -e "\n${S_NOTICE}Parsing ${PROPS_FILE_NAME}:\n"
|
||||
|
||||
V_MAJOR_REGEX='^([a-zA-Z\.]+major)=([0-9]+)$'
|
||||
V_MINOR_REGEX='^([a-zA-Z\.]+minor)=([0-9]+)$'
|
||||
V_PATCH_REGEX='^([a-zA-Z\.]+patch)=([0-9]+)$'
|
||||
V_BUILD_REGEX='^([a-zA-Z\.]+build)=([0-9]+)$'
|
||||
|
||||
PROPS_FILE_NEW=""
|
||||
|
||||
LAST_LINE=$(wc -l <$PROPS_FILE_NAME)
|
||||
CURRENT_LINE=0
|
||||
|
||||
while read -r line; do
|
||||
CURRENT_LINE=$((CURRENT_LINE + 1))
|
||||
|
||||
if [[ $line =~ $V_MAJOR_REGEX ]]; then
|
||||
updated="${BASH_REMATCH[1]}=${V_MAJOR}"
|
||||
echo "Found major, replacing: $line -> $updated"
|
||||
PROPS_FILE_NEW+=$updated
|
||||
elif [[ $line =~ $V_MINOR_REGEX ]]; then
|
||||
updated="${BASH_REMATCH[1]}=${V_MINOR}"
|
||||
echo "Found minor, replacing: $line -> $updated"
|
||||
PROPS_FILE_NEW+=$updated
|
||||
elif [[ $line =~ $V_PATCH_REGEX ]]; then
|
||||
updated="${BASH_REMATCH[1]}=${V_PATCH}"
|
||||
echo "Found patch, replacing: $line -> $updated"
|
||||
PROPS_FILE_NEW+=$updated
|
||||
elif [[ $line =~ $V_BUILD_REGEX ]]; then
|
||||
updated="${BASH_REMATCH[1]}=${V_BUILD_COUNTER}"
|
||||
echo "Found build, replacing: $line -> $updated"
|
||||
PROPS_FILE_NEW+=$updated
|
||||
else
|
||||
PROPS_FILE_NEW+="$line"
|
||||
fi
|
||||
|
||||
if [[ $CURRENT_LINE -ne $LAST_LINE ]]; then
|
||||
PROPS_FILE_NEW+="\n"
|
||||
fi
|
||||
|
||||
done <"$PROPS_FILE_NAME"
|
||||
|
||||
echo -e "$PROPS_FILE_NEW" >"$PROPS_FILE_NAME"
|
||||
git add "$PROPS_FILE_NAME"
|
||||
|
||||
echo -e "\n${I_OK} ${S_NOTICE}Updated [${S_NORM}${PROPS_FILE_NAME}${S_NOTICE}] file"
|
||||
}
|
||||
|
||||
# Update a version file that can be parsed by third-parties, e.g. F-Droid
|
||||
do-versionfile() {
|
||||
[ -f VERSION ] && ACTION_MSG="Updated" || ACTION_MSG="Created"
|
||||
|
||||
echo "${V_NAME} ${V_CODE}" >VERSION # Create file
|
||||
echo -e "\n${I_OK} ${S_NOTICE}${ACTION_MSG} [${S_NORM}VERSION${S_NOTICE}] file"
|
||||
|
||||
# Stage file for commit
|
||||
git add VERSION
|
||||
}
|
||||
|
||||
# Does the release branch already exist?
|
||||
check-branch-exist() {
|
||||
[ "$FLAG_NOBRANCH" = true ] && return
|
||||
|
||||
BRANCH_MSG=$(git rev-parse --verify "${REL_PREFIX}${V_NAME}" 2>&1)
|
||||
if [ "$?" -eq 0 ]; then
|
||||
echo -e "\n${I_STOP} ${S_ERROR}Error: Branch <${S_NORM}${REL_PREFIX}${V_NAME}${S_ERROR}> already exists!\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Create release branch if desired
|
||||
do-branch() {
|
||||
[ "$FLAG_NOBRANCH" = true ] && return
|
||||
|
||||
echo -e "\n${S_NOTICE}Creating new release branch..."
|
||||
|
||||
BRANCH_MSG=$(git branch "${REL_PREFIX}${V_NAME}" 2>&1)
|
||||
if [ ! "$?" -eq 0 ]; then
|
||||
echo -e "\n${I_STOP} ${S_ERROR}Error\n$BRANCH_MSG\n"
|
||||
exit 1
|
||||
else
|
||||
BRANCH_MSG=$(git checkout "${REL_PREFIX}${V_NAME}" 2>&1)
|
||||
echo -e "\n${I_OK} ${S_NOTICE}${BRANCH_MSG}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Stage & commit all files modified by this script
|
||||
do-commit() {
|
||||
[ "$FLAG_NOCOMMIT" = true ] && return
|
||||
|
||||
echo -e "\n${S_NOTICE}Committing..."
|
||||
COMMIT_MSG=$(git commit -m "${GIT_MSG}" 2>&1)
|
||||
if [ ! "$?" -eq 0 ]; then
|
||||
echo -e "\n${I_STOP} ${S_ERROR}Error\n$COMMIT_MSG\n"
|
||||
exit 1
|
||||
else
|
||||
echo -e "\n${I_OK} ${S_NOTICE}$COMMIT_MSG"
|
||||
fi
|
||||
}
|
||||
|
||||
# Pushes files + tags to remote repo. Changes are staged by earlier functions
|
||||
do-push() {
|
||||
[ "$FLAG_NOCOMMIT" = true ] && return
|
||||
|
||||
if [ "$FLAG_PUSH" = true ]; then
|
||||
CONFIRM="Y"
|
||||
else
|
||||
echo -ne "\n${S_QUESTION}Push tags to <${S_NORM}${PUSH_DEST}${S_QUESTION}>? [${S_NORM}N/y${S_QUESTION}]: "
|
||||
read CONFIRM
|
||||
fi
|
||||
|
||||
case "$CONFIRM" in
|
||||
[yY][eE][sS] | [yY])
|
||||
echo -e "\n${S_NOTICE}Pushing files + tags to <${S_NORM}${PUSH_DEST}${S_NOTICE}>..."
|
||||
|
||||
PUSH_MSG=$(git push "${PUSH_DEST}" v"$V_NAME" 2>&1) # Push new tag
|
||||
PUSH_MSG+="\n"
|
||||
PUSH_MSG+=$(git push 2>&1) # Push new tag
|
||||
|
||||
if [ ! "$?" -eq 0 ]; then
|
||||
echo -e "\n${I_STOP} ${S_WARN}Warning\n$PUSH_MSG"
|
||||
# exit 1
|
||||
else
|
||||
echo -e "\n${I_OK} ${S_NOTICE}$PUSH_MSG"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
#### Initiate Script ###########################
|
||||
|
||||
check-commits-exist
|
||||
|
||||
# Process and prepare
|
||||
process-arguments "$@"
|
||||
process-version
|
||||
|
||||
GIT_MSG+="${V_NAME}"
|
||||
|
||||
check-branch-exist
|
||||
check-tag-exists
|
||||
|
||||
echo -e "\n${S_LIGHT}––––––"
|
||||
|
||||
# Update steps
|
||||
do-version-properties
|
||||
do-versionfile
|
||||
do-branch
|
||||
do-commit
|
||||
create-tag "${V_NAME}" "${REL_NOTE}"
|
||||
do-push
|
||||
|
||||
echo -e "\n${S_LIGHT}––––––"
|
||||
echo -e "\n${I_OK} ${S_NOTICE}"Bumped $([ -n "${V_PREV}" ] && echo "${V_PREV} –>" || echo "to ") "$V_NAME"
|
||||
echo -e "\n${GREEN}Done ${I_END}\n"
|
||||
@@ -0,0 +1,6 @@
|
||||
### Updated by release.sh ###
|
||||
project.versioning.major=2
|
||||
project.versioning.minor=4
|
||||
project.versioning.patch=0
|
||||
project.versioning.build=0
|
||||
#############################
|
||||
Reference in New Issue
Block a user