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)
|
get() = rawFlags.isBitSet(0)
|
||||||
|
|
||||||
override val isBeingWorn: Boolean
|
override val isBeingWorn: Boolean
|
||||||
get() = !rawStatus.isBitSet(5)
|
get() = rawStatus.isBitSet(5)
|
||||||
|
|
||||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsMax
|
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@@ -21,15 +20,4 @@ class SingleApplePodsTest : BaseAirPodsTest() {
|
|||||||
rawSuffix shouldBe 0x40.toUByte()
|
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 shouldBe 0x03.toUByte()
|
||||||
|
|
||||||
rawStatus.isBitSet(5) shouldBe false
|
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") {
|
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 shouldBe 0x23.toUByte()
|
||||||
|
|
||||||
rawStatus.isBitSet(5) shouldBe true
|
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 eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.retryWhen
|
import kotlinx.coroutines.flow.retryWhen
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class BillingClientConnectionProvider @Inject constructor(
|
class BillingClientConnectionProvider @Inject constructor(
|
||||||
@@ -48,36 +48,40 @@ class BillingClientConnectionProvider @Inject constructor(
|
|||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val connectionResult = suspendCoroutine<BillingResult> { continuation ->
|
|
||||||
log(TAG, VERBOSE) { "startConnection(...)" }
|
log(TAG, VERBOSE) { "startConnection(...)" }
|
||||||
client.startConnection(object : BillingClientStateListener {
|
client.startConnection(object : BillingClientStateListener {
|
||||||
override fun onBillingSetupFinished(result: BillingResult) {
|
override fun onBillingSetupFinished(result: BillingResult) {
|
||||||
log(TAG, VERBOSE) {
|
log(TAG, VERBOSE) {
|
||||||
"onBillingSetupFinished(code=${result.responseCode}, message=${result.debugMessage})"
|
"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() {
|
override fun onBillingServiceDisconnected() {
|
||||||
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
|
log(TAG, VERBOSE) { "onBillingServiceDisconnected() " }
|
||||||
close(CancellationException("Billing service disconnected"))
|
close(BillingException("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)
|
|
||||||
|
|
||||||
log(TAG) { "Awaiting close." }
|
log(TAG) { "Awaiting close." }
|
||||||
awaitClose {
|
awaitClose {
|
||||||
@@ -89,26 +93,28 @@ class BillingClientConnectionProvider @Inject constructor(
|
|||||||
val connection: Flow<BillingClientConnection> = connectionProvider
|
val connection: Flow<BillingClientConnection> = connectionProvider
|
||||||
.setupCommonEventHandlers(TAG) { "connection" }
|
.setupCommonEventHandlers(TAG) { "connection" }
|
||||||
.retryWhen { cause, attempt ->
|
.retryWhen { cause, attempt ->
|
||||||
|
log(TAG) { "Billing client connection error: ${cause.asLog()}" }
|
||||||
|
|
||||||
if (cause is CancellationException) {
|
if (cause is CancellationException) {
|
||||||
log(TAG) { "BillingClient connection cancelled." }
|
log(TAG) { "BillingClient connection cancelled." }
|
||||||
return@retryWhen false
|
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
|
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." }
|
log(TAG) { "Got BILLING_UNAVAILABLE while trying to connect client." }
|
||||||
return@retryWhen false
|
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*" }
|
log(TAG) { "Will retry BillingClient connection... *sigh*" }
|
||||||
delay(3000 * attempt)
|
delay(3000 * attempt)
|
||||||
true
|
true
|
||||||
|
|||||||
+1
-3
@@ -2,9 +2,7 @@ package eu.darken.capod.common.upgrade.core.client
|
|||||||
|
|
||||||
import com.android.billingclient.api.BillingResult
|
import com.android.billingclient.api.BillingResult
|
||||||
|
|
||||||
class BillingClientException(val result: BillingResult) : Exception() {
|
class BillingClientException(val result: BillingResult) : BillingException(result.debugMessage) {
|
||||||
override val message: String?
|
|
||||||
get() = result.debugMessage
|
|
||||||
|
|
||||||
override fun toString(): String =
|
override fun toString(): String =
|
||||||
"BillingClientException(code=${result.responseCode}, message=${result.debugMessage})"
|
"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.PodDeviceCache
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.pods.core.*
|
import eu.darken.capod.pods.core.*
|
||||||
|
import finish2
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
@@ -39,7 +40,7 @@ class WidgetProvider : AppWidgetProvider() {
|
|||||||
|
|
||||||
private fun executeAsync(
|
private fun executeAsync(
|
||||||
tag: String,
|
tag: String,
|
||||||
timeout: Duration = Duration.ofSeconds(8),
|
timeout: Duration = Duration.ofSeconds(7),
|
||||||
block: suspend () -> Unit
|
block: suspend () -> Unit
|
||||||
) {
|
) {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
@@ -52,7 +53,7 @@ class WidgetProvider : AppWidgetProvider() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log(TAG, ERROR) { "executeAsync($tag) failed: ${e.asLog()}" }
|
log(TAG, ERROR) { "executeAsync($tag) failed: ${e.asLog()}" }
|
||||||
} finally {
|
} finally {
|
||||||
asyncBarrier.finish()
|
asyncBarrier.finish2()
|
||||||
val stop = System.currentTimeMillis()
|
val stop = System.currentTimeMillis()
|
||||||
log(TAG, VERBOSE) { "executeAsync($tag) DONE (${stop - start}ms) " }
|
log(TAG, VERBOSE) { "executeAsync($tag) DONE (${stop - start}ms) " }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.bluetooth.BluetoothDevice
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.hilt.work.HiltWorker
|
import androidx.hilt.work.HiltWorker
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
@@ -59,10 +60,16 @@ class MonitorWorker @AssistedInject constructor(
|
|||||||
log(TAG, VERBOSE) { "init(): workerId=$id" }
|
log(TAG, VERBOSE) { "init(): workerId=$id" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||||
|
return monitorNotifications.getForegroundInfo(null)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun doWork(): Result = try {
|
override suspend fun doWork(): Result = try {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
log(TAG, VERBOSE) { "Executing $inputData now (runAttemptCount=$runAttemptCount)" }
|
log(TAG, VERBOSE) { "Executing $inputData now (runAttemptCount=$runAttemptCount)" }
|
||||||
|
|
||||||
|
setForeground(monitorNotifications.getForegroundInfo(null))
|
||||||
|
|
||||||
doDoWork()
|
doDoWork()
|
||||||
|
|
||||||
val duration = System.currentTimeMillis() - start
|
val duration = System.currentTimeMillis() - start
|
||||||
@@ -91,7 +98,6 @@ class MonitorWorker @AssistedInject constructor(
|
|||||||
|
|
||||||
val monitorJob = podMonitor.mainDevice
|
val monitorJob = podMonitor.mainDevice
|
||||||
.setupCommonEventHandlers(TAG) { "PodMonitor" }
|
.setupCommonEventHandlers(TAG) { "PodMonitor" }
|
||||||
.onStart { setForeground(monitorNotifications.getForegroundInfo(null)) }
|
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.throttleLatest(1000)
|
.throttleLatest(1000)
|
||||||
.onEach { currentDevice ->
|
.onEach { currentDevice ->
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import eu.darken.capod.common.hasApiLevel
|
|||||||
import eu.darken.capod.common.notifications.PendingIntentCompat
|
import eu.darken.capod.common.notifications.PendingIntentCompat
|
||||||
import eu.darken.capod.main.ui.MainActivity
|
import eu.darken.capod.main.ui.MainActivity
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@@ -29,6 +31,7 @@ class MonitorNotifications @Inject constructor(
|
|||||||
private val notificationViewFactory: MonitorNotificationViewFactory
|
private val notificationViewFactory: MonitorNotificationViewFactory
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val builderLock = Mutex()
|
||||||
private val builder: NotificationCompat.Builder
|
private val builder: NotificationCompat.Builder
|
||||||
|
|
||||||
init {
|
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) {
|
if (device == null) {
|
||||||
return builder.apply {
|
return builder.apply {
|
||||||
setCustomContentView(null)
|
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")
|
@SuppressLint("InlinedApi")
|
||||||
private fun NotificationCompat.Builder.toForegroundInfo(): ForegroundInfo = if (hasApiLevel(29)) {
|
private fun NotificationCompat.Builder.toForegroundInfo(): ForegroundInfo = if (hasApiLevel(29)) {
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
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("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.Kotlin.core}")
|
||||||
classpath("com.google.dagger:hilt-android-gradle-plugin:${Versions.Dagger.core}")
|
classpath("com.google.dagger:hilt-android-gradle-plugin:${Versions.Dagger.core}")
|
||||||
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.AndroidX.Navigation.core}")
|
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.AndroidX.Navigation.core}")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ repositories {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.android.tools.build:gradle:7.2.2")
|
implementation("com.android.tools.build:gradle:7.3.1")
|
||||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10")
|
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
|
||||||
implementation("com.squareup:javapoet:1.13.0")
|
implementation("com.squareup:javapoet:1.13.0")
|
||||||
}
|
}
|
||||||
@@ -84,14 +84,14 @@ fun DependencyHandlerScope.addBaseWorkManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun DependencyHandlerScope.addBaseAndroid() {
|
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.annotation:annotation:1.4.0")
|
||||||
implementation("androidx.collection:collection-ktx:1.2.0")
|
implementation("androidx.collection:collection-ktx:1.2.0")
|
||||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DependencyHandlerScope.addBaseAndroidUi() {
|
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.constraintlayout:constraintlayout:2.1.3")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ object Versions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Kotlin {
|
object Kotlin {
|
||||||
const val core = "1.7.10"
|
const val core = "1.8.0"
|
||||||
const val coroutines = "1.6.2"
|
const val coroutines = "1.6.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,11 +13,11 @@ object Versions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Dagger {
|
object Dagger {
|
||||||
const val core = "2.42"
|
const val core = "2.45"
|
||||||
}
|
}
|
||||||
|
|
||||||
object Moshi {
|
object Moshi {
|
||||||
const val core = "1.13.0"
|
const val core = "1.14.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
object AndroidX {
|
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.
|
* Automatically connect phone and AirPods.
|
||||||
* Show popup when case is opened.
|
* 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.
|
CAPod is ad-free. Some features require an in-app purchase.
|
||||||
|
|
||||||
Most popular AirPods and Beats devices are supported.
|
Most popular AirPods and Beats devices are supported.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
### Updated by release.sh ###
|
### Updated by release.sh ###
|
||||||
project.versioning.major=2
|
project.versioning.major=2
|
||||||
project.versioning.minor=8
|
project.versioning.minor=8
|
||||||
project.versioning.patch=2
|
project.versioning.patch=4
|
||||||
project.versioning.build=0
|
project.versioning.build=0
|
||||||
#############################
|
#############################
|
||||||
|
|||||||
Reference in New Issue
Block a user