Skip to content

Commit 2970b9c

Browse files
committed
Charging: Revert the conditional charge rules ahead of device verification
Reverts the 12 commits of PR #72. The feature merged before the planned physical device pass (Bluetooth transitions, reboot, process kill, Shizuku loss, permission revocation, GrapheneOS latch); it returns via a follow-up PR once that verification has run.
1 parent e1eb7b0 commit 2970b9c

34 files changed

Lines changed: 32 additions & 4372 deletions

app/src/debug/java/eu/darken/amply/screenshots/ScreenshotContent.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ private fun DashboardShot(state: DashboardUiState) = PreviewWrapper {
9191
onAlarmEnabledChange = {},
9292
onAlarmTargetChange = {},
9393
onFixNotifications = {},
94-
onOpenConditions = {},
9594
onOpenBatteryHub = {},
9695
onRetryCapture = {},
9796
onPinWidget = {},

app/src/main/AndroidManifest.xml

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,6 @@
99
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
1010
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
1111

12-
<!-- Charge conditions: a Bluetooth-device rule needs the connect/disconnect broadcasts and the
13-
bonded-device list. Runtime-requested on API 31+, install-time below that. -->
14-
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
15-
<uses-permission
16-
android:name="android.permission.BLUETOOTH"
17-
android:maxSdkVersion="30" />
18-
1912
<queries>
2013
<package android:name="moe.shizuku.privileged.api" />
2114
<package android:name="com.google.android.settings.intelligence" />
@@ -111,15 +104,6 @@
111104
android:resource="@xml/amply_widget_info" />
112105
</receiver>
113106

114-
<receiver
115-
android:name=".rules.core.BluetoothRuleReceiver"
116-
android:exported="true">
117-
<intent-filter>
118-
<action android:name="android.bluetooth.device.action.ACL_CONNECTED" />
119-
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED" />
120-
</intent-filter>
121-
</receiver>
122-
123107
<receiver
124108
android:name=".fullcharge.core.BootReceiver"
125109
android:directBootAware="false"

app/src/main/java/eu/darken/amply/fullcharge/core/ChargeSessionManager.kt

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,9 @@ class ChargeSessionManager @Inject constructor(
2222
) {
2323
private val mutex = Mutex()
2424

25-
/**
26-
* [restoreOverride] replaces the observed current policy as the session's restore target. Set by
27-
* the charge-conditions layer when a rule currently owns the policy: what is configured right now
28-
* is the *rule's* temporary override, so restoring to it at the end of the session would make the
29-
* override permanent and lose the user's own baseline. The session record is durable and survives
30-
* process death, so handing it the true baseline makes it the single owner of that restore.
31-
*/
32-
/**
33-
* [afterPersisted] runs in the window between the session record being persisted and the override
34-
* write. That is the only correct place for the charge-conditions handoff: the session durably
35-
* owes the restore from the moment its record exists, and clearing rule ownership any later
36-
* leaves both layers claiming the baseline across a write that can fail, be cancelled, or die
37-
* with the process. It must not throw — it runs inside the session mutex.
38-
*/
3925
suspend fun begin(
4026
nowMillis: Long = System.currentTimeMillis(),
4127
pluggedAtStart: Boolean? = null,
42-
restoreOverride: ChargePolicy? = null,
43-
afterPersisted: (suspend () -> Unit)? = null,
4428
): ApplyResult = mutex.withLock {
4529
sessionStore.currentSession()?.let {
4630
return@withLock ApplyResult(
@@ -94,7 +78,7 @@ class ChargeSessionManager @Inject constructor(
9478
message = "The current OEM charging mode is not recognized; refusing to overwrite it",
9579
)
9680
}
97-
val restorePolicy = restoreOverride ?: (decision as SessionStartDecision.Start).restorePolicy
81+
val restorePolicy = (decision as SessionStartDecision.Start).restorePolicy
9882

9983
// Persist recovery state before removing the limit. Stamp this process's identity so a later
10084
// pickup can tell whether the session survived a process death (interruption detection), and a
@@ -114,7 +98,6 @@ class ChargeSessionManager @Inject constructor(
11498
// that instruction until the replug is observed.
11599
overrideAwaitingReplug = adapter?.policyLatchesAtPlug == true && pluggedAtStart != false,
116100
)
117-
afterPersisted?.invoke()
118101
val result = repository.applyTemporary(overridePolicy)
119102
if (result.success) {
120103
// Reconcile the persist-first conservative flag with the repository's authoritative

app/src/main/java/eu/darken/amply/fullcharge/core/ChargeSessionService.kt

Lines changed: 2 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ import eu.darken.amply.common.debug.logging.logTag
2626
import eu.darken.amply.main.core.SurfaceUpdater
2727
import eu.darken.amply.monitor.core.ChargeMonitorTick
2828
import eu.darken.amply.monitor.core.ChargeMonitorWatcher
29-
import eu.darken.amply.rules.core.PlugKind
30-
import eu.darken.amply.rules.core.RuleApplier
3129
import kotlinx.coroutines.CancellationException
3230
import kotlinx.coroutines.CoroutineScope
3331
import 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

Comments
 (0)