@@ -26,8 +26,6 @@ import eu.darken.amply.common.debug.logging.logTag
2626import eu.darken.amply.main.core.SurfaceUpdater
2727import eu.darken.amply.monitor.core.ChargeMonitorTick
2828import eu.darken.amply.monitor.core.ChargeMonitorWatcher
29- import eu.darken.amply.rules.core.PlugKind
30- import eu.darken.amply.rules.core.RuleApplier
3129import kotlinx.coroutines.CancellationException
3230import kotlinx.coroutines.CoroutineScope
3331import kotlinx.coroutines.Dispatchers
@@ -51,7 +49,6 @@ class ChargeSessionService : Service() {
5149 @Inject lateinit var interruptionAssessor: InterruptionAssessor
5250 @Inject lateinit var processIdentity: ProcessIdentity
5351 @Inject lateinit var bootCountProvider: BootCountProvider
54- @Inject lateinit var ruleApplier: RuleApplier
5552
5653 // Optional, permission-free battery observers (charge alarm, …), contributed via @IntoSet.
5754 @Inject lateinit var watchers: Set < @JvmSuppressWildcards ChargeMonitorWatcher >
@@ -76,8 +73,6 @@ class ChargeSessionService : Service() {
7673 // Written under the dispatch lock, but read by the battery receiver/monitor loop outside it.
7774 @Volatile private var recoveryJob: Job ? = null
7875 private var settingObserverRegistered = false
79- // Whether this service instance has already swept the Bluetooth profile proxies (see evaluateRules).
80- private var bluetoothReconciled = false
8176 @Volatile private var restoring = false
8277 // One-shot interruption assessment for a freshly resumed persisted session: set when
8378 // beginOrResume picks up an existing session, consumed by the first battery evaluation, and
@@ -208,33 +203,7 @@ class ChargeSessionService : Service() {
208203 log(TAG ) { " Starting a one-time full-charge session" }
209204 // A brand-new session in this process is not an interruption; drop any stale assessment.
210205 pendingSessionAssessment = null
211- // A conditional rule may currently own the policy. Hand its baseline to the session: what
212- // is configured right now is the rule's temporary override, so the session must restore
213- // the user's real policy, not the override.
214- val ruleBaseline = ruleApplier.readActiveBaseline()
215- val result = manager.begin(
216- pluggedAtStart = currentPlugged(),
217- restoreOverride = ruleBaseline,
218- // Handed over inside begin(), in the window between the session record being
219- // persisted and the override write: from that moment the session owes the restore,
220- // and clearing any later would leave both layers claiming the baseline across a
221- // write that can fail or die with the process.
222- //
223- // Contained, because begin() runs this between persisting the session and writing
224- // the override: letting a DataStore failure escape would abort the start after the
225- // record exists, stranding a session whose override write never ran. Stale rule
226- // bookkeeping is the far cheaper failure — the next evaluation clears it against the
227- // live session anyway.
228- afterPersisted = {
229- try {
230- ruleApplier.clearActiveAfterSessionPersist()
231- } catch (e: CancellationException ) {
232- throw e
233- } catch (e: Exception ) {
234- log(TAG , Logging .Priority .WARN ) { " Rule ownership handoff failed: ${e.message} " }
235- }
236- },
237- )
206+ val result = manager.begin(pluggedAtStart = currentPlugged())
238207 if (! result.success) {
239208 log(TAG , Logging .Priority .WARN ) { " Unable to start full-charge session: ${result.message} " }
240209 if (fullChargeStore.currentSession() != null ) SessionNotifications .showRecovery(this )
@@ -302,51 +271,6 @@ class ChargeSessionService : Service() {
302271 }
303272 }
304273
305- /* *
306- * Run one conditional-charge-rule evaluation.
307- *
308- * A first-class step of the evaluation path, NOT watcher work: watcher ticks are optional and
309- * bounded by a per-watcher budget, while a rule write changes the charging policy and owes a
310- * restore — it must never be cut short. It runs *after* the safety-critical session decisions
311- * above (a restore must never queue behind it) and before the optional watchers.
312- *
313- * Failure is contained the same way a watcher's is: the rules layer must not be able to stop a
314- * battery evaluation.
315- */
316- private suspend fun evaluateRules (
317- plugged : Boolean ,
318- plugKind : PlugKind ? ,
319- sessionActive : Boolean ,
320- reconcileBluetooth : Boolean = false,
321- ) {
322- try {
323- ruleApplier.evaluate(
324- plugged = plugged,
325- plugKind = plugKind,
326- sessionActive = sessionActive,
327- // Always on this instance's first pass, whichever command brought the service up: a
328- // process that was not running missed every ACL broadcast in the meantime, and the
329- // stored snapshot is only as good as the last one it received. After that the
330- // receiver keeps it current and the sweep would just cost Binder round-trips.
331- reconcileBluetooth = reconcileBluetooth || ! bluetoothReconciled,
332- )
333- bluetoothReconciled = true
334- } catch (e: CancellationException ) {
335- throw e
336- } catch (e: Exception ) {
337- log(TAG , Logging .Priority .ERROR ) { " Rule evaluation failed: ${e.message} " }
338- }
339- }
340-
341- /* * Current plug state and charger class from the sticky broadcast, for a command-driven pass. */
342- private fun currentPlug (): Pair <Boolean , PlugKind ?> {
343- val raw = runCatching {
344- registerReceiver(null , IntentFilter (Intent .ACTION_BATTERY_CHANGED ))
345- ?.getIntExtra(BatteryManager .EXTRA_PLUGGED , 0 )
346- }.getOrNull() ? : 0
347- return (raw != 0 ) to plugKindOf(raw)
348- }
349-
350274 /* *
351275 * Deliver a battery tick to every optional watcher. Evaluations are already serialized (single
352276 * evaluation consumer under the [coordinator]'s lock), so no extra lock is needed. Each watcher is bounded
@@ -431,9 +355,7 @@ class ChargeSessionService : Service() {
431355 // An in-flight evaluation can outlive the quiesce in startRecovery; never race recovery.
432356 if (recoveryJob?.isActive == true ) return
433357 val battery = intent ? : registerReceiver(null , IntentFilter (Intent .ACTION_BATTERY_CHANGED ))
434- val pluggedRaw = battery?.getIntExtra(BatteryManager .EXTRA_PLUGGED , 0 ) ? : 0
435- val plugged = pluggedRaw != 0
436- val plugKind = plugKindOf(pluggedRaw)
358+ val plugged = (battery?.getIntExtra(BatteryManager .EXTRA_PLUGGED , 0 ) ? : 0 ) != 0
437359 val status = battery?.getIntExtra(
438360 BatteryManager .EXTRA_STATUS ,
439361 BatteryManager .BATTERY_STATUS_UNKNOWN ,
@@ -525,8 +447,6 @@ class ChargeSessionService : Service() {
525447 )
526448 }
527449 }
528- // A live session outranks the rules layer; this pass only reconciles its bookkeeping.
529- evaluateRules(plugged, plugKind, sessionActive = true )
530450 // Non-restore session tick: let watchers observe it (the alarm claims the cycle here).
531451 dispatchWatchers(plugged, percent, status, sessionOwned = true , battery, observedAtElapsed)
532452 return
@@ -541,7 +461,6 @@ class ChargeSessionService : Service() {
541461 val gestureEnabled = fullChargeStore.isQuickFullChargeEnabled()
542462 val adapter = if (gestureEnabled) adapterRegistry.select().adapter else null
543463 if (! gestureEnabled || adapter?.reconnectGestureSupported != true ) {
544- evaluateRules(plugged, plugKind, sessionActive = false )
545464 dispatchWatchers(plugged, percent, status, sessionOwned = false , battery, observedAtElapsed)
546465 // Gesture inactive: keep running only if a watcher still wants the service, showing the
547466 // quiet monitoring notification instead of the gesture cue.
@@ -600,7 +519,6 @@ class ChargeSessionService : Service() {
600519 " plugged=$plugged percent=$percent status=$status "
601520 }
602521 }
603- evaluateRules(plugged, plugKind, sessionActive = false )
604522 // A triggering tick is a deliberate full charge about to begin, so the alarm must treat it
605523 // as session-owned and NOT fire "unplug now" on the very reconnect that started the charge.
606524 dispatchWatchers(
@@ -669,20 +587,6 @@ class ChargeSessionService : Service() {
669587 when (action) {
670588 ACTION_RESTORE -> if (recoveryJob?.isActive != true ) restoreAndContinue()
671589 ACTION_MONITOR -> if (recoveryJob?.isActive != true ) continueGestureOrStop()
672- // A rule edit or a Bluetooth connection change. Gated on recovery like ACTION_MONITOR: a
673- // rule write must never race the boot-recovery convergence loop. No forced Bluetooth
674- // sweep here — the once-per-service-instance one in evaluateRules already covers the
675- // missed-broadcast case, and sweeping on every ACL event risks a lagging profile proxy
676- // writing a just-disconnected address back over the receiver's fresher snapshot.
677- ACTION_EVALUATE_RULES -> if (recoveryJob?.isActive != true ) {
678- val (plugged, plugKind) = currentPlug()
679- evaluateRules(
680- plugged = plugged,
681- plugKind = plugKind,
682- sessionActive = fullChargeStore.currentSession() != null ,
683- )
684- continueGestureOrStop()
685- }
686590 ACTION_START -> {
687591 // A user-initiated session supersedes boot recovery; the new session
688592 // overwrites the policy anyway. Join so a cancelled re-write cannot
@@ -740,19 +644,6 @@ class ChargeSessionService : Service() {
740644 // Assess whether this recovery is picking up work a dead process left behind, BEFORE the
741645 // flow mutates the pending target.
742646 val pickup = interruptionAssessor.captureRecoveryPickup()
743- // A persisted session already carries the baseline as its restore target, so rule
744- // bookkeeping left ACTIVE beside it is stale: recovery is about to write policies, and
745- // the rules layer must not come back afterwards claiming to own the result.
746- if (fullChargeStore.currentSession() != null ) {
747- try {
748- ruleApplier.clearActiveAfterSessionPersist()
749- } catch (e: CancellationException ) {
750- // A cancelled recovery job must actually stop here, not carry on into the flow.
751- throw e
752- } catch (e: Exception ) {
753- log(TAG , Logging .Priority .WARN ) { " Rule ownership clear failed: ${e.message} " }
754- }
755- }
756647 val result = BootRecoveryFlow (recoveryHooks).run ()
757648 log(TAG ) { " Boot recovery outcome: ${result.outcome} " }
758649 // A converged recovery restored the protective policy, so clear any lingering alarm.
@@ -881,17 +772,6 @@ class ChargeSessionService : Service() {
881772 // instead of leaving charging in whatever transient state the session had. An explicit persistent
882773 // choice is new owed work, so it gets a fresh work id.
883774 fullChargeStore.setPendingRecoveryTarget(policy, UUID .randomUUID().toString(), currentWorkProvenance())
884- // Suspend the rules layer here, in the same persisted-intent step and BEFORE the write: a
885- // process death between the write and a post-success suspension would leave the explicit
886- // policy configured with every rule still armed to overwrite it on the next tick.
887- val (pluggedNow, plugKindNow) = currentPlug()
888- try {
889- ruleApplier.suspendMatchingCohort(pluggedNow, plugKindNow)
890- } catch (e: CancellationException ) {
891- throw e
892- } catch (e: Exception ) {
893- log(TAG , Logging .Priority .WARN ) { " Rule suspension failed: ${e.message} " }
894- }
895775 restoring = true
896776 coordinator.close()
897777 try {
@@ -1002,23 +882,6 @@ class ChargeSessionService : Service() {
1002882 const val ACTION_RECOVER = " eu.darken.amply.action.RECOVER_CHARGE_LIMIT"
1003883 const val ACTION_CHECK = " eu.darken.amply.action.CHECK_CHARGE_STATE"
1004884 const val ACTION_SET_PERSISTENT_POLICY = " eu.darken.amply.action.SET_PERSISTENT_POLICY"
1005- const val ACTION_EVALUATE_RULES = " eu.darken.amply.action.EVALUATE_CHARGE_RULES"
1006885 const val EXTRA_TARGET_POLICY = " eu.darken.amply.extra.TARGET_POLICY"
1007-
1008- /* *
1009- * The charger class from `BatteryManager.EXTRA_PLUGGED`. Null for unplugged and for a value
1010- * this build does not know — an unknown charger must not silently satisfy a rule that names
1011- * specific charger types.
1012- */
1013- // BATTERY_PLUGGED_DOCK postdates minSdk, but these are compile-time constants that inline —
1014- // an older platform simply never reports the value.
1015- @Suppress(" InlinedApi" )
1016- internal fun plugKindOf (extraPlugged : Int ): PlugKind ? = when (extraPlugged) {
1017- BatteryManager .BATTERY_PLUGGED_AC -> PlugKind .AC
1018- BatteryManager .BATTERY_PLUGGED_USB -> PlugKind .USB
1019- BatteryManager .BATTERY_PLUGGED_WIRELESS -> PlugKind .WIRELESS
1020- BatteryManager .BATTERY_PLUGGED_DOCK -> PlugKind .DOCK
1021- else -> null
1022- }
1023886 }
1024887}
0 commit comments