Policy, bank, and usage algorithms¶
This page describes the algorithms in the Android Java implementation rather than an idealized design. It gives upper bounds from explicit code limits and input sizes, not device benchmarks. See the shared policy, sleep, bank-credit, and offline concepts for user-facing explanations.
Local policy evaluation¶
The access path can be read as a series of gates. This diagram summarizes the combined policy and bank controllers; the evaluator described below handles the policy/allowance branch itself.
flowchart TD
A[App access is reevaluated] --> B{Essential or unrestricted?}
B -->|Yes| Y[Allow under these rules]
B -->|No| C{Selected by current policy?}
C -->|No| Y
C -->|Yes| D{Protected sleep now?}
D -->|Yes| E{Valid explicit sleep override?}
E -->|Yes| Y
E -->|No| N[Keep app blocked]
D -->|No| F{Accepted allowance still valid?}
F -->|Yes| Y
F -->|No| G{Active bank session with credit and healthy usage evidence?}
G -->|Yes| Y
G -->|No| N
Expiry triggers reevaluation, not an unconditional block. A notification or an old approval receipt cannot skip these gates. Missing or unverifiable policy does not turn a previously managed app into an unlisted app.
AccessPolicy.evaluate takes an already validated policy, a package name, a zoned time, and the state of a verified allowance. It computes sleep windows for five local calendar mornings: the previous day and the next three days as well as today. Looking behind one morning catches a sleep interval that crosses midnight; looking ahead provides the next schedule boundary.
For a package, the evaluator checks unrestricted membership first, then scans groups and their package arrays. An unlisted package is allowed by this selected-package policy. A listed package is blocked during sleep unless a valid allowance explicitly carries sleep-override scope and the policy enables that override. Outside sleep, a listed package needs an active allowance in this evaluator; bank access is a separate controller path, not a side effect of this decision.
The policy schema caps groups at 32, group package arrays at 256, the total unique selected and unrestricted packages at 256, and both date exceptions and custom holiday dates at 366. Let \(U\) be the unrestricted entries, \(G\) the groups, \(P\) the package entries across groups, \(E\) the date exceptions, and \(H\) the custom holiday dates. The evaluator scans unrestricted entries once, group membership once, and the exception and holiday lists for a fixed five dates. Its work is therefore:
The fixed-size window list, computed Colombian holiday set, and temporary membership state use bounded auxiliary memory. The validator intentionally supports the schema keywords used by this policy format; it is not a complete implementation of every JSON Schema keyword.
Time-bank ledger¶
TimeBankLedger stores a balance, a partial eligible-time remainder, and the marker for the most recent daily floor per bank. During intervals outside protected sleep, every bank whose group remains configured by current policy accrues one configured deposit after each configured eligible interval, even when no spend session is active. Here, “eligible” means outside the policy sleep schedule; it does not mean that the phone is awake, unlocked, or screen-on. Screen and activity evidence separately gate debits. Deposits stop at capacity, and reaching capacity clears the partial remainder.
At a transition out of sleep, the ledger raises a balance to the daily minimum if the balance is lower, once for a later local calendar date. It leaves a balance above that minimum alone. This is a floor, not a midnight reset: unused credit does not expire each day. A policy update can lower the configured capacity, in which case stored balances are clamped to the new cap.
Reconciliation advances from the persisted cursor through schedule boundaries. The loop is bounded by MAX_BOUNDARIES_PER_ADVANCE (5,000); if it cannot reach the requested time within that limit, it throws instead of claiming a complete reconciliation. nextRefill has the same boundary cap, and nextCredit checks at most eight transitions for the next useful daily floor.
For a cost model, let \(K\leq 5000\) be the boundaries processed, \(B\) the number of bank records retained in local state, \(G\) the number of groups in the current policy, and \(A\) the work of one policy evaluation above. Refill and floor processing visit bank records and test whether each remains configured by scanning current groups. The conservative upper bound is:
The map can retain balances for groups removed from a policy, so \(B\) counts those historical records too. Policy and schema limits bound the current group/package data, but they do not make a growing retained-bank map constant-size. The 5,000-boundary failure is an explicit safety stop, not an approximation.
Foreground usage accounting¶
ForegroundUsage.read requests UsageEvents for a window no longer than 30 minutes and begins its query at most 24 hours before the requested start so it can rebuild the state at the boundary. It streams raw OS events, keeps only event types it understands, and fails if the mapped event list grows beyond 10,000 entries. Screen-off, keyguard, multi-package ambiguity, denied access, and query failures are represented as health states rather than guessed as foreground use.
UsageEventReducer sorts the mapped events by timestamp and tracks resumed and visible activity identities. For each event it recomputes which package is eligible and detects sustained overlap. Let \(R\) be raw OS events returned by the platform, \(F\leq 10{,}000\) mapped events, and \(V\) the maximum number of tracked activity identities during reduction. Reading costs \(O(R)\) platform iteration; reduction costs \(O(F\log F+F\cdot V)\), because eligibility checks build or scan sets across tracked identities. In the worst case \(V\leq F\), so the reducer’s time bound is \(O(F^2)\); its event list and state use \(O(F+V)\) memory. The code does not impose a separate numeric cap on raw OS events \(R\), so the time-window and 10,000 mapped-event limit are not a hard bound on platform iteration.
Bank debits require a fresh, acceptable sample and only apply to intervals whose package belongs to the active bank group. For each such interval, the debit is bounded by measured duration, available balance, and remaining session allowance:
where \(\Delta t_i\) is the known foreground interval, \(b_i\) is the balance immediately before it, and \(s_i\) is the configured session limit remaining. Unknown time is not substituted with elapsed wall time.
In words: charge the smallest of measured use, available credit and remaining session time. This rule remains understandable even if a math renderer is unavailable.
Worked example¶
Consider an illustrative policy, with no connection to an account or device. It adds 5 minutes after each 60 minutes outside the protected sleep schedule, caps a bank at 90 minutes, and sets a 10-minute daily floor. Refill accrues whether or not a spend session is active; the 60 minutes do not mean screen-on use. If an unused bank has 4 minutes at the end of a sleep window, the floor raises it to 10 minutes; if it has 14 minutes, it remains at 14. After 60 eligible minutes outside sleep, a bank below capacity gains 5 minutes.
Suppose a person explicitly starts a session with 12 minutes available. A fresh, unambiguous Usage Events sample identifies 7 minutes in an app belonging to that bank’s group, and 20 minutes remain in the session allowance. The equation debits 7 minutes and leaves 5. If a reboot or ambiguous event history leaves a final interval uncertain, that tail is recorded for recovery but is not charged. The session closes until the person starts it again.
The session uses a five-second renewable access lease. The monitor renews it only after a successful evidence query, and settles recent events with a 1.5-second delay. On abrupt reboot, the ledger-side recovery calculation caps any saved-but-unmeasured tail at 6.5 seconds and leaves it uncharged; saved earlier debits remain. This is not a guaranteed physical suspension deadline: Android may delay alarm delivery, extending actual unsuspended time. See limitations for this platform boundary. A policy change ends an active bank session, which must be started again under the new rules.
Tradeoffs¶
The accounting favors a defensible charge over charging every second that the clock advanced. That can undercount use during event loss or a reboot, but it prevents uncertain time from consuming credit. App-level Usage Events are evidence of foreground activity, not a screen recording or proof of attention. This mechanism does not measure individual browser tabs or content inside an app.