mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
11
Commits
v2.8.2-rc0
...
v2.8.4-rc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b5b59c41b | ||
|
|
8df7c19e84 | ||
|
|
88b0069b94 | ||
|
|
3b8ddc9f06 | ||
|
|
708c7f38ec | ||
|
|
139daa1cc2 | ||
|
|
b85ac778c0 | ||
|
|
a3e2c9e61d | ||
|
|
ad78a0570c | ||
|
|
7e6ee274d8 | ||
|
|
daf7411c36 |
@@ -0,0 +1,10 @@
|
||||
import android.content.BroadcastReceiver
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
|
||||
fun BroadcastReceiver.PendingResult.finish2(): Boolean = try {
|
||||
finish()
|
||||
true
|
||||
} catch (e: IllegalStateException) {
|
||||
log { "BroadcastReceiver.PendingResult.finish() failed: $e" }
|
||||
false
|
||||
}
|
||||
@@ -34,7 +34,7 @@ data class AirPodsMax(
|
||||
get() = rawFlags.isBitSet(0)
|
||||
|
||||
override val isBeingWorn: Boolean
|
||||
get() = !rawStatus.isBitSet(5)
|
||||
get() = rawStatus.isBitSet(5)
|
||||
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -21,15 +20,4 @@ class SingleApplePodsTest : BaseAirPodsTest() {
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
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
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
isBeingWorn shouldBe true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,13 +81,13 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
||||
rawStatus shouldBe 0x03.toUByte()
|
||||
|
||||
rawStatus.isBitSet(5) shouldBe false
|
||||
isBeingWorn shouldBe true
|
||||
isBeingWorn shouldBe false
|
||||
}
|
||||
create<AirPodsMax>("07 19 01 0A 20 23 07 80 03 03 65 1F 28 32 D0 D9 71 43 00 9A 40 E7 6B EA 6C 2C FB") {
|
||||
rawStatus shouldBe 0x23.toUByte()
|
||||
|
||||
rawStatus.isBitSet(5) shouldBe true
|
||||
isBeingWorn shouldBe false
|
||||
isBeingWorn shouldBe true
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
-38
@@ -14,15 +14,15 @@ import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@Singleton
|
||||
class BillingClientConnectionProvider @Inject constructor(
|
||||
@@ -48,36 +48,40 @@ class BillingClientConnectionProvider @Inject constructor(
|
||||
}
|
||||
}.build()
|
||||
|
||||
val connectionResult = suspendCoroutine<BillingResult> { continuation ->
|
||||
log(TAG, VERBOSE) { "startConnection(...)" }
|
||||
client.startConnection(object : BillingClientStateListener {
|
||||
override fun onBillingSetupFinished(result: BillingResult) {
|
||||
log(TAG, VERBOSE) {
|
||||
"onBillingSetupFinished(code=${result.responseCode}, message=${result.debugMessage})"
|
||||
|
||||
log(TAG, VERBOSE) { "startConnection(...)" }
|
||||
client.startConnection(object : BillingClientStateListener {
|
||||
override fun onBillingSetupFinished(result: BillingResult) {
|
||||
log(TAG, VERBOSE) {
|
||||
"onBillingSetupFinished(code=${result.responseCode}, message=${result.debugMessage})"
|
||||
}
|
||||
|
||||
when (result.responseCode) {
|
||||
BillingResponseCode.OK -> {
|
||||
val connection = BillingClientConnection(client, purchasePublisher)
|
||||
|
||||
trySendBlocking(connection)
|
||||
|
||||
launch {
|
||||
try {
|
||||
purchasePublisher.value = connection.queryPurchases()
|
||||
log(TAG) { "Initial IAP query successful." }
|
||||
} catch (e: Exception) {
|
||||
log(TAG, ERROR) { "Initial IAP query failed:\n${e.asLog()}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
close(BillingClientException(result))
|
||||
}
|
||||
continuation.resume(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBillingServiceDisconnected() {
|
||||
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
|
||||
close(CancellationException("Billing service disconnected"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val billingClientConnection = when (connectionResult.responseCode) {
|
||||
BillingResponseCode.OK -> BillingClientConnection(client, purchasePublisher)
|
||||
else -> throw BillingClientException(connectionResult)
|
||||
}
|
||||
|
||||
try {
|
||||
purchasePublisher.value = billingClientConnection.queryPurchases()
|
||||
log(TAG) { "Initial IAP query successful." }
|
||||
} catch (e: Exception) {
|
||||
log(TAG, ERROR) { "Initial IAP query failed:\n${e.asLog()}" }
|
||||
}
|
||||
|
||||
send(billingClientConnection)
|
||||
override fun onBillingServiceDisconnected() {
|
||||
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
|
||||
close(BillingException("Billing service disconnected"))
|
||||
}
|
||||
})
|
||||
|
||||
log(TAG) { "Awaiting close." }
|
||||
awaitClose {
|
||||
@@ -89,26 +93,28 @@ class BillingClientConnectionProvider @Inject constructor(
|
||||
val connection: Flow<BillingClientConnection> = connectionProvider
|
||||
.setupCommonEventHandlers(TAG) { "connection" }
|
||||
.retryWhen { cause, attempt ->
|
||||
log(TAG) { "Billing client connection error: ${cause.asLog()}" }
|
||||
|
||||
if (cause is CancellationException) {
|
||||
log(TAG) { "BillingClient connection cancelled." }
|
||||
return@retryWhen false
|
||||
}
|
||||
if (attempt > 5) {
|
||||
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
|
||||
|
||||
if (cause !is BillingException) {
|
||||
log(TAG, WARN) { "Unknown exception type: $cause" }
|
||||
return@retryWhen false
|
||||
}
|
||||
if (cause !is BillingClientException) {
|
||||
log(TAG, WARN) { "Unknown BillingClient exception type: $cause" }
|
||||
return@retryWhen false
|
||||
} else {
|
||||
log(TAG) { "BillingClient exception: $cause; ${cause.result}" }
|
||||
}
|
||||
|
||||
if (cause.result.responseCode == BillingResponseCode.BILLING_UNAVAILABLE) {
|
||||
if (cause is BillingClientException && cause.result.responseCode == BillingResponseCode.BILLING_UNAVAILABLE) {
|
||||
log(TAG) { "Got BILLING_UNAVAILABLE while trying to connect client." }
|
||||
return@retryWhen false
|
||||
}
|
||||
|
||||
if (attempt > 5) {
|
||||
log(TAG, WARN) { "Reached attempt limit: $attempt due to $cause" }
|
||||
return@retryWhen false
|
||||
}
|
||||
|
||||
log(TAG) { "Will retry BillingClient connection... *sigh*" }
|
||||
delay(3000 * attempt)
|
||||
true
|
||||
|
||||
+1
-3
@@ -2,9 +2,7 @@ package eu.darken.capod.common.upgrade.core.client
|
||||
|
||||
import com.android.billingclient.api.BillingResult
|
||||
|
||||
class BillingClientException(val result: BillingResult) : Exception() {
|
||||
override val message: String?
|
||||
get() = result.debugMessage
|
||||
class BillingClientException(val result: BillingResult) : BillingException(result.debugMessage) {
|
||||
|
||||
override fun toString(): String =
|
||||
"BillingClientException(code=${result.responseCode}, message=${result.debugMessage})"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package eu.darken.capod.common.upgrade.core.client
|
||||
|
||||
open class BillingException(override val message: String) : Exception()
|
||||
@@ -22,6 +22,7 @@ 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 finish2
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
@@ -39,7 +40,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
|
||||
private fun executeAsync(
|
||||
tag: String,
|
||||
timeout: Duration = Duration.ofSeconds(8),
|
||||
timeout: Duration = Duration.ofSeconds(7),
|
||||
block: suspend () -> Unit
|
||||
) {
|
||||
val start = System.currentTimeMillis()
|
||||
@@ -52,7 +53,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
} catch (e: Exception) {
|
||||
log(TAG, ERROR) { "executeAsync($tag) failed: ${e.asLog()}" }
|
||||
} finally {
|
||||
asyncBarrier.finish()
|
||||
asyncBarrier.finish2()
|
||||
val stop = System.currentTimeMillis()
|
||||
log(TAG, VERBOSE) { "executeAsync($tag) DONE (${stop - start}ms) " }
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.bluetooth.BluetoothDevice
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
@@ -59,10 +60,16 @@ class MonitorWorker @AssistedInject constructor(
|
||||
log(TAG, VERBOSE) { "init(): workerId=$id" }
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
return monitorNotifications.getForegroundInfo(null)
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result = try {
|
||||
val start = System.currentTimeMillis()
|
||||
log(TAG, VERBOSE) { "Executing $inputData now (runAttemptCount=$runAttemptCount)" }
|
||||
|
||||
setForeground(monitorNotifications.getForegroundInfo(null))
|
||||
|
||||
doDoWork()
|
||||
|
||||
val duration = System.currentTimeMillis() - start
|
||||
@@ -91,7 +98,6 @@ class MonitorWorker @AssistedInject constructor(
|
||||
|
||||
val monitorJob = podMonitor.mainDevice
|
||||
.setupCommonEventHandlers(TAG) { "PodMonitor" }
|
||||
.onStart { setForeground(monitorNotifications.getForegroundInfo(null)) }
|
||||
.distinctUntilChanged()
|
||||
.throttleLatest(1000)
|
||||
.onEach { currentDevice ->
|
||||
|
||||
@@ -20,6 +20,8 @@ import eu.darken.capod.common.hasApiLevel
|
||||
import eu.darken.capod.common.notifications.PendingIntentCompat
|
||||
import eu.darken.capod.main.ui.MainActivity
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@@ -29,6 +31,7 @@ class MonitorNotifications @Inject constructor(
|
||||
private val notificationViewFactory: MonitorNotificationViewFactory
|
||||
) {
|
||||
|
||||
private val builderLock = Mutex()
|
||||
private val builder: NotificationCompat.Builder
|
||||
|
||||
init {
|
||||
@@ -56,7 +59,7 @@ class MonitorNotifications @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun getBuilder(device: PodDevice?): NotificationCompat.Builder {
|
||||
private fun getBuilder(device: PodDevice?): NotificationCompat.Builder {
|
||||
if (device == null) {
|
||||
return builder.apply {
|
||||
setCustomContentView(null)
|
||||
@@ -76,9 +79,13 @@ class MonitorNotifications @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun getNotification(podDevice: PodDevice?): Notification = getBuilder(podDevice).build()
|
||||
suspend fun getNotification(podDevice: PodDevice?): Notification = builderLock.withLock {
|
||||
getBuilder(podDevice).build()
|
||||
}
|
||||
|
||||
fun getForegroundInfo(podDevice: PodDevice?): ForegroundInfo = getBuilder(podDevice).toForegroundInfo()
|
||||
suspend fun getForegroundInfo(podDevice: PodDevice?): ForegroundInfo = builderLock.withLock {
|
||||
getBuilder(podDevice).toForegroundInfo()
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun NotificationCompat.Builder.toForegroundInfo(): ForegroundInfo = if (hasApiLevel(29)) {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.2.2")
|
||||
classpath("com.android.tools.build:gradle:7.3.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.Kotlin.core}")
|
||||
classpath("com.google.dagger:hilt-android-gradle-plugin:${Versions.Dagger.core}")
|
||||
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.AndroidX.Navigation.core}")
|
||||
|
||||
@@ -8,7 +8,7 @@ repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
implementation("com.android.tools.build:gradle:7.2.2")
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10")
|
||||
implementation("com.android.tools.build:gradle:7.3.1")
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
|
||||
implementation("com.squareup:javapoet:1.13.0")
|
||||
}
|
||||
@@ -84,14 +84,14 @@ fun DependencyHandlerScope.addBaseWorkManager() {
|
||||
}
|
||||
|
||||
fun DependencyHandlerScope.addBaseAndroid() {
|
||||
implementation("androidx.core:core-ktx:1.8.0")
|
||||
implementation("androidx.core:core-ktx:1.10.0-rc01")
|
||||
implementation("androidx.annotation:annotation:1.4.0")
|
||||
implementation("androidx.collection:collection-ktx:1.2.0")
|
||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||
}
|
||||
|
||||
fun DependencyHandlerScope.addBaseAndroidUi() {
|
||||
implementation("androidx.appcompat:appcompat:1.6.0-alpha04")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
||||
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ object Versions {
|
||||
}
|
||||
|
||||
object Kotlin {
|
||||
const val core = "1.7.10"
|
||||
const val core = "1.8.0"
|
||||
const val coroutines = "1.6.2"
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ object Versions {
|
||||
}
|
||||
|
||||
object Dagger {
|
||||
const val core = "2.42"
|
||||
const val core = "2.45"
|
||||
}
|
||||
|
||||
object Moshi {
|
||||
const val core = "1.13.0"
|
||||
const val core = "1.14.0"
|
||||
}
|
||||
|
||||
object AndroidX {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
Bugfixes and performance improvements.
|
||||
¯\_(ツ)_/¯
|
||||
@@ -0,0 +1,2 @@
|
||||
Bugfixes and performance improvements.
|
||||
¯\_(ツ)_/¯
|
||||
@@ -10,6 +10,9 @@ Features:
|
||||
* Automatically connect phone and AirPods.
|
||||
* Show popup when case is opened.
|
||||
|
||||
CAPod has a Wear-OS version that shows the status of the pod device that is closest to you.
|
||||
CAPod for Wear-OS is a stand-alone app and does not require a phone.
|
||||
|
||||
CAPod is ad-free. Some features require an in-app purchase.
|
||||
|
||||
Most popular AirPods and Beats devices are supported.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
### Updated by release.sh ###
|
||||
project.versioning.major=2
|
||||
project.versioning.minor=8
|
||||
project.versioning.patch=2
|
||||
project.versioning.patch=4
|
||||
project.versioning.build=0
|
||||
#############################
|
||||
|
||||
Reference in New Issue
Block a user