diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 63eea704..385a54d7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -141,6 +141,20 @@
android:name=".monitor.core.worker.MonitorService"
android:foregroundServiceType="connectedDevice"
android:exported="false" />
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/app/src/main/java/eu/darken/capod/main/ui/components/AncModeUi.kt b/app/src/main/java/eu/darken/capod/main/ui/components/AncModeUi.kt
index 50fdbbdb..40d67b02 100644
--- a/app/src/main/java/eu/darken/capod/main/ui/components/AncModeUi.kt
+++ b/app/src/main/java/eu/darken/capod/main/ui/components/AncModeUi.kt
@@ -1,6 +1,7 @@
package eu.darken.capod.main.ui.components
import android.content.Context
+import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.AutoAwesome
@@ -28,3 +29,11 @@ fun AapSetting.AncMode.Value.icon(): ImageVector = when (this) {
AapSetting.AncMode.Value.ADAPTIVE -> Icons.TwoTone.AutoAwesome
}
+@DrawableRes
+fun AapSetting.AncMode.Value.iconDrawableRes(): Int = when (this) {
+ AapSetting.AncMode.Value.OFF -> R.drawable.ic_anc_off
+ AapSetting.AncMode.Value.ON -> R.drawable.ic_anc_on
+ AapSetting.AncMode.Value.TRANSPARENCY -> R.drawable.ic_anc_transparency
+ AapSetting.AncMode.Value.ADAPTIVE -> R.drawable.ic_anc_adaptive
+}
+
diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt
index f87e8e7f..e7076d02 100644
--- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt
+++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt
@@ -23,6 +23,7 @@ import eu.darken.capod.main.core.MonitorMode
import eu.darken.capod.main.core.PermissionTool
import eu.darken.capod.monitor.core.DeviceMonitor
import eu.darken.capod.monitor.core.PodDevice
+import eu.darken.capod.monitor.core.tierRank
import eu.darken.capod.monitor.core.worker.MonitorControl
import eu.darken.capod.pods.core.apple.aap.AapConnectionManager
import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand
@@ -175,7 +176,7 @@ class OverviewViewModel @Inject constructor(
val profiledDevices: List by lazy {
devices.filter { it.profileId != null }.sortedWith(
- compareBy { deviceTierRank(it) }
+ compareBy { it.tierRank() }
.thenBy { profileOrder[it.profileId] ?: Int.MAX_VALUE }
)
}
@@ -284,12 +285,5 @@ class OverviewViewModel @Inject constructor(
companion object {
private const val FREE_DEVICE_LIMIT = 1
private val TAG = logTag("Overview", "VM")
-
- /** Connection tier rank for sorting: lower = higher priority. */
- internal fun deviceTierRank(device: PodDevice): Int = when {
- device.isSystemConnected -> 0
- device.isLive -> 1
- else -> 2
- }
}
}
diff --git a/app/src/main/java/eu/darken/capod/main/ui/tile/AncTileSendCoordinator.kt b/app/src/main/java/eu/darken/capod/main/ui/tile/AncTileSendCoordinator.kt
new file mode 100644
index 00000000..15921891
--- /dev/null
+++ b/app/src/main/java/eu/darken/capod/main/ui/tile/AncTileSendCoordinator.kt
@@ -0,0 +1,142 @@
+package eu.darken.capod.main.ui.tile
+
+import eu.darken.capod.common.bluetooth.BluetoothAddress
+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.pods.core.apple.aap.AapConnectionManager
+import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand
+import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * Process-scoped trailing-edge debouncer for tile-driven `SetAncMode` commands.
+ *
+ * The QS panel collapses after each tap, destroying the [AncTileService] instance.
+ * If the pending-job state lived on the service, taps across consecutive panel
+ * sessions wouldn't cancel each other — every tap would fire a separate
+ * `SetAncMode`, overwhelming the AAP verification loop and triggering
+ * "Rejected after retry" storms that leave the device unresponsive until the
+ * app restarts. Keeping the job here, on a `@Singleton`, means a tap in panel
+ * session B can cancel the deferred send queued by panel session A.
+ */
+@Singleton
+class AncTileSendCoordinator @Inject constructor(
+ @AppScope private val appScope: CoroutineScope,
+ private val aapManager: AapConnectionManager,
+) {
+
+ private val lock = Any()
+ private val pendingJobs = mutableMapOf()
+ private val timeoutJobs = mutableMapOf()
+
+ private val _pendingModes = MutableStateFlow