Compare commits

...
Author SHA1 Message Date
darken 65f9e1d9db Release: 2.17.6-rc0 2025-07-03 00:19:40 +02:00
darken c97c8f4852 Refactor: Introduce EdgeToEdgeHelper for consistent inset handling
This commit introduces an `EdgeToEdgeHelper` class to centralize and simplify the application of window insets for edge-to-edge display.

Key changes:
- Created `EdgeToEdgeHelper` to manage padding based on system bar insets.
- Migrated various Fragments (`OverviewFragment`, `OnboardingFragment`, `SettingsFragment`, `TroubleShooterFragment`) to use `EdgeToEdgeHelper` for consistent padding.
- Removed manual inset handling from `Activity2` and `Fragment2` as `EdgeToEdgeHelper` now manages this.
- Enabled edge-to-edge display in `MainActivity`.
- Minor updates to `BillingClientConnection` for product details fetching and `build.gradle.kts` for configuration.
2025-07-03 00:14:58 +02:00
darken 6b2073eff2 Update AGP, SDK, and BillingClient
This commit updates:
- Android Gradle Plugin to 8.11.0
- Compile/Target SDK to 36
- Google Play Billing Library to 8.0.0

The `BillingClient` is updated to use the new `PendingPurchasesParams` and `QueryPurchasesParams` APIs.
2025-07-03 00:14:58 +02:00
darken 2b08d9a4d0 Release: 2.17.5-rc0 2025-06-30 14:13:41 +02:00
darken 977f135c50 Allow empty keys to clear IRK/EncKey
This commit modifies the handling of Identity Resolving Key (IRK) and Encryption Key (EncKey) to allow users to clear these values by providing an empty input.

Specifically:
- In `GeneralSettingsFragment.kt`, the `onKey` callbacks for both IRK and EncKey dialogs now use `takeIf { it.isNotEmpty() }` after converting the input hex string to a byte array. This ensures that an empty input results in `null` being set for the respective key.
- In `PodMonitor.kt`, when determining the main device, `mainDeviceIdentityKey.value` is now checked with `takeIf { it.isNotEmpty() }` to ensure an empty IRK is treated as no IRK being configured.
- In `AppleFactory.kt`, when attempting to decrypt the private payload, `mainDeviceEncryptionKey.value` is now checked with `takeIf { it.isNotEmpty() }` to ensure an empty EncKey prevents decryption attempts.
2025-06-30 14:12:32 +02:00
darken 3c777d377e Only use IRK match for main device if IRK is configured
This commit modifies `PodMonitor.kt` to ensure that the `isIRKMatch` flag is only used to determine the main device if an Identity Resolving Key (IRK) is actually configured in the general settings.

If no IRK is set, the main device determination will fall back to other logic, preventing a device from being incorrectly selected as "main" solely based on an IRK match when no specific IRK is being looked for.
2025-06-30 14:12:32 +02:00
darken b98994f2f1 Revert "Fix IRK-only mode to prevent heuristic fallback + auto battery optimization"
This reverts commit ca5041b27d.
2025-06-30 14:12:32 +02:00
wnerhs 85860a53bf Fix IRK-only mode to prevent heuristic fallback + auto battery optimization
- Prevent heuristic device detection when IRK/ENC keys are configured
- Maintain last IRK-matched device state instead of fallback
- Auto-enable LOW_POWER scan mode when IRK device is found
- Fixes false positives in crowded places
2025-06-30 14:12:32 +02:00
23 changed files with 116 additions and 48 deletions
+1 -1
View File
@@ -1 +1 @@
2.17.4-rc0 21704000
2.17.6-rc0 21706000
@@ -3,7 +3,6 @@ package eu.darken.capod.common.uix
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.lifecycle.LiveData
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
@@ -16,8 +15,6 @@ abstract class Activity2 : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
log(tag, VERBOSE) { "onCreate(savedInstanceState=$savedInstanceState)" }
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows( window, false )
}
override fun onResume() {
@@ -7,8 +7,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
@@ -42,17 +40,6 @@ abstract class Fragment2(@LayoutRes val layoutRes: Int?) : Fragment(layoutRes ?:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
log(tag, VERBOSE) { "onViewCreated(view=$view, savedInstanceState=$savedInstanceState)" }
super.onViewCreated(view, savedInstanceState)
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val systemWindowInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(
v.paddingLeft,
systemWindowInsets.top,
v.paddingRight,
0
)
insets
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
@@ -198,9 +198,10 @@ class PodMonitor @Inject constructor(
}
private fun determineMainDevice(pods: List<PodDevice>): PodDevice? {
val irkHit = pods.filterIsInstance<ApplePods>().firstOrNull { it.flags.isIRKMatch }
if (irkHit != null) {
log(TAG) { "Main device determined via IRK: $irkHit" }
val identityKey = generalSettings.mainDeviceIdentityKey.value?.takeIf { it.isNotEmpty() }
if (identityKey != null) {
val irkHit = pods.filterIsInstance<ApplePods>().firstOrNull { it.flags.isIRKMatch }
log(TAG) { "IRK is configured, main device irkHit=$irkHit" }
return irkHit
}
@@ -71,7 +71,7 @@ class AppleFactory @Inject constructor(
if (!isIrkMatch) return@run null
if (proximityMessage.data.size != ProximityPairing.PAIRING_MESSAGE_LENGTH) return@run null
val encKey = generalSettings.mainDeviceEncryptionKey.value
val encKey = generalSettings.mainDeviceEncryptionKey.value?.takeIf { it.isNotEmpty() }
if (encKey == null) return@run null
val encrypted = proximityMessage.data.takeLast(16).toUByteArray().toByteArray()
+6 -6
View File
@@ -10,7 +10,7 @@ apply(plugin = "androidx.navigation.safeargs.kotlin")
android {
compileSdk = ProjectConfig.compileSdk
namespace = "${ProjectConfig.packageName}"
namespace = ProjectConfig.packageName
defaultConfig {
applicationId = ProjectConfig.packageName
@@ -122,9 +122,9 @@ android {
unitTests {
isIncludeAndroidResources = true
}
tasks.withType<Test> {
useJUnitPlatform()
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
}
@@ -151,6 +151,6 @@ dependencies {
addTesting()
"gplayImplementation"("com.android.billingclient:billing:7.1.1")
"gplayImplementation"("com.android.billingclient:billing-ktx:7.1.1")
"gplayImplementation"("com.android.billingclient:billing:8.0.0")
"gplayImplementation"("com.android.billingclient:billing-ktx:8.0.0")
}
@@ -8,6 +8,7 @@ import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryPurchasesParams
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.log
@@ -39,8 +40,12 @@ data class BillingClientConnection(
.setupCommonEventHandlers(TAG) { "purchases" }
suspend fun queryPurchases(): Collection<Purchase> {
val params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build()
val (result: BillingResult, purchases) = suspendCoroutine<Pair<BillingResult, Collection<Purchase>?>> { continuation ->
client.queryPurchasesAsync(BillingClient.SkuType.INAPP) { result, purchases ->
client.queryPurchasesAsync(params) { result, purchases ->
continuation.resume(result to purchases)
}
}
@@ -80,9 +85,10 @@ data class BillingClientConnection(
val params = QueryProductDetailsParams.newBuilder().setProductList(listOf(productDetails)).build()
val (result, details) = suspendCoroutine<Pair<BillingResult, Collection<ProductDetails>?>> { continuation ->
client.queryProductDetailsAsync(params) { result, skuDetails ->
continuation.resume(result to skuDetails)
val (result, details) = suspendCoroutine<Pair<BillingResult, List<ProductDetails>>> { continuation ->
client.queryProductDetailsAsync(params) { billingResult, queryResult ->
val productDetailsList = queryResult.productDetailsList ?: emptyList()
continuation.resume(billingResult to productDetailsList)
}
}
@@ -92,7 +98,7 @@ data class BillingClientConnection(
if (!result.isSuccess) throw BillingResultException(result)
if (details.isNullOrEmpty()) throw IllegalStateException("Unknown SKU, no details available.")
if (details.isEmpty()) throw IllegalStateException("Unknown SKU, no details available.")
return Sku.Details(sku, details)
}
@@ -5,6 +5,7 @@ import com.android.billingclient.api.BillingClient.BillingResponseCode
import com.android.billingclient.api.BillingClient.newBuilder
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.PendingPurchasesParams
import com.android.billingclient.api.Purchase
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.debug.logging.Logging.Priority.*
@@ -33,7 +34,11 @@ class BillingClientConnectionProvider @Inject constructor(
val purchasePublisher = MutableStateFlow<Collection<Purchase>>(emptySet())
val client = newBuilder(context).apply {
enablePendingPurchases()
enablePendingPurchases(
PendingPurchasesParams.newBuilder()
.enableOneTimeProducts()
.build()
)
setListener { result, purchases ->
if (result.isSuccess) {
log(TAG) {
@@ -0,0 +1,33 @@
package eu.darken.capod.common
import android.app.Activity
import android.view.View
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import eu.darken.capod.common.debug.logging.logTag
class EdgeToEdgeHelper(activity: Activity) {
private val tag = logTag("EdgeToEdge", "$activity")
fun insetsPadding(
view: View,
left: Boolean = false,
top: Boolean = false,
right: Boolean = false,
bottom: Boolean = false,
) {
ViewCompat.setOnApplyWindowInsetsListener(view) { v: View, insets: WindowInsetsCompat ->
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(
if (left) systemBars.left else v.paddingLeft,
if (top) systemBars.top else v.paddingTop,
if (right) systemBars.right else v.paddingRight,
if (bottom) systemBars.bottom else v.paddingBottom,
)
insets
}
}
}
@@ -1,6 +1,7 @@
package eu.darken.capod.main.ui
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import dagger.hilt.android.AndroidEntryPoint
@@ -17,9 +18,9 @@ class MainActivity : Activity2() {
private val navController by lazy { supportFragmentManager.findNavController(R.id.nav_host) }
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
installSplashScreen()
enableEdgeToEdge()
ui = MainActivityBinding.inflate(layoutInflater)
setContentView(ui.root)
@@ -5,6 +5,7 @@ import android.view.View
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.R
import eu.darken.capod.common.EdgeToEdgeHelper
import eu.darken.capod.common.PrivacyPolicy
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.uix.Fragment3
@@ -22,6 +23,9 @@ class OnboardingFragment : Fragment3(R.layout.onboarding_fragment) {
@Inject lateinit var webpageTool: WebpageTool
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
EdgeToEdgeHelper(requireActivity()).apply {
insetsPadding(ui.root, left = true, right = true, top = true, bottom = true)
}
ui.goPrivacyPolicy.setOnClickListener { webpageTool.open(PrivacyPolicy.URL) }
ui.continueAction.setOnClickListener { vm.finishOnboarding() }
super.onViewCreated(view, savedInstanceState)
@@ -8,12 +8,11 @@ import android.text.SpannableStringBuilder
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.BuildConfig
import eu.darken.capod.R
import eu.darken.capod.common.EdgeToEdgeHelper
import eu.darken.capod.common.colorString
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.lists.differ.update
@@ -49,6 +48,11 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
EdgeToEdgeHelper(requireActivity()).apply {
insetsPadding(ui.root, left = true, right = true)
insetsPadding(ui.toolbar, top = true)
insetsPadding(ui.list, bottom = false)
}
ui.apply {
list.setupDefaults(adapter, dividers = false)
}
@@ -60,14 +64,17 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
vm.goToSettings()
true
}
R.id.menu_item_donate -> {
vm.onUpgrade()
true
}
R.id.menu_item_upgrade -> {
vm.onUpgrade()
true
}
else -> false
}
}
@@ -93,6 +100,7 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
)
)
}
Permission.SYSTEM_ALERT_WINDOW -> {
awaitingPermission = true
startActivity(
@@ -102,6 +110,7 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
)
)
}
else -> {
permissionLauncher.launch(it.permissionId)
}
@@ -123,6 +132,7 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
getString(eu.darken.capod.common.R.string.app_name)
}
}
UpgradeRepo.Type.FOSS -> {
if (info.isPro) {
getString(eu.darken.capod.common.R.string.app_name_foss)
@@ -2,10 +2,7 @@ package eu.darken.capod.main.ui.settings
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ListView
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.viewModels
import androidx.preference.Preference
@@ -13,6 +10,7 @@ import androidx.preference.PreferenceFragmentCompat
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.R
import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.EdgeToEdgeHelper
import eu.darken.capod.common.uix.Fragment2
import eu.darken.capod.common.viewbinding.viewBinding
import eu.darken.capod.databinding.SettingsFragmentBinding
@@ -38,6 +36,11 @@ class SettingsFragment : Fragment2(R.layout.settings_fragment),
) : Parcelable
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
EdgeToEdgeHelper(requireActivity()).apply {
insetsPadding(ui.root, left = true, right = true)
insetsPadding(ui.toolbar, top = true)
insetsPadding(ui.contentFrame, bottom = true)
}
childFragmentManager.addOnBackStackChangedListener {
val backStackCnt = childFragmentManager.backStackEntryCount
val newScreenInfo = when {
@@ -55,7 +55,9 @@ class GeneralSettingsFragment : PreferenceFragment3() {
AirPodKeyInputDialog(requireContext()).create(
mode = AirPodKeyInputDialog.Mode.IRK,
current = generalSettings.mainDeviceIdentityKey.value?.toHex() ?: "",
onKey = { generalSettings.mainDeviceIdentityKey.value = it.fromHex() },
onKey = { raw ->
generalSettings.mainDeviceIdentityKey.value = raw.fromHex().takeIf { it.isNotEmpty() }
},
onGuide = { webpageTool.open("https://github.com/d4rken-org/capod/wiki/airpod-Keys") }
).show()
true
@@ -64,7 +66,9 @@ class GeneralSettingsFragment : PreferenceFragment3() {
AirPodKeyInputDialog(requireContext()).create(
mode = AirPodKeyInputDialog.Mode.ENC,
current = generalSettings.mainDeviceEncryptionKey.value?.toHex() ?: "",
onKey = { generalSettings.mainDeviceEncryptionKey.value = it.fromHex() },
onKey = { raw ->
generalSettings.mainDeviceEncryptionKey.value = raw.fromHex().takeIf { it.isNotEmpty() }
},
onGuide = { webpageTool.open("https://github.com/d4rken-org/capod/wiki/airpod-Keys") }
).show()
true
@@ -8,6 +8,7 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.R
import eu.darken.capod.common.EdgeToEdgeHelper
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.navigation.popBackStack
import eu.darken.capod.common.uix.Fragment3
@@ -26,6 +27,11 @@ class TroubleShooterFragment : Fragment3(R.layout.troubleshooter_fragment) {
@Inject lateinit var webpageTool: WebpageTool
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
EdgeToEdgeHelper(requireActivity()).apply {
insetsPadding(ui.root, left = true, right = true)
insetsPadding(ui.toolbar, top = true)
insetsPadding(ui.scrollView, bottom = true)
}
ui.toolbar.apply {
setupWithNavController(findNavController())
}
@@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="32dp">
android:layout_margin="32dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@@ -18,6 +18,7 @@
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/scrollView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
+1 -1
View File
@@ -8,7 +8,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.10.1")
classpath("com.android.tools.build:gradle:8.11.0")
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}")
+1 -1
View File
@@ -8,7 +8,7 @@ repositories {
mavenCentral()
}
dependencies {
implementation("com.android.tools.build:gradle:8.10.1")
implementation("com.android.tools.build:gradle:8.11.0")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21")
implementation("com.squareup:javapoet:1.13.0")
}
+2 -2
View File
@@ -11,8 +11,8 @@ object ProjectConfig {
const val packageName = "eu.darken.capod"
const val minSdk = 26
const val compileSdk = 34
const val targetSdk = 34
const val compileSdk = 36
const val targetSdk = 36
object Version {
val versionProperties = Properties().apply {
@@ -0,0 +1,5 @@
🐛 Bug fixes, 🚀 performance boosts, maybe even ✨ new features.
Changelog: https://capod.darken.eu/changelog
FYI: Its just me here — thanks for understanding if replies take a bit. ¯\_(ツ)_/¯
@@ -0,0 +1,5 @@
🐛 Bug fixes, 🚀 performance boosts, maybe even ✨ new features.
Changelog: https://capod.darken.eu/changelog
FYI: Its just me here — thanks for understanding if replies take a bit. ¯\_(ツ)_/¯
+1 -1
View File
@@ -1,6 +1,6 @@
### Updated by release.sh ###
project.versioning.major=2
project.versioning.minor=17
project.versioning.patch=4
project.versioning.patch=6
project.versioning.build=0
#############################