• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.tare;
18 
19 import static android.text.format.DateUtils.DAY_IN_MILLIS;
20 
21 import static com.android.server.tare.EconomicPolicy.REGULATION_BASIC_INCOME;
22 import static com.android.server.tare.EconomicPolicy.REGULATION_BIRTHRIGHT;
23 import static com.android.server.tare.EconomicPolicy.REGULATION_DEMOTION;
24 import static com.android.server.tare.EconomicPolicy.REGULATION_PROMOTION;
25 import static com.android.server.tare.EconomicPolicy.REGULATION_WEALTH_RECLAMATION;
26 import static com.android.server.tare.EconomicPolicy.TYPE_ACTION;
27 import static com.android.server.tare.EconomicPolicy.TYPE_REWARD;
28 import static com.android.server.tare.EconomicPolicy.eventToString;
29 import static com.android.server.tare.EconomicPolicy.getEventType;
30 import static com.android.server.tare.TareUtils.appToString;
31 import static com.android.server.tare.TareUtils.cakeToString;
32 import static com.android.server.tare.TareUtils.getCurrentTimeMillis;
33 
34 import android.annotation.NonNull;
35 import android.annotation.Nullable;
36 import android.content.Context;
37 import android.content.pm.ApplicationInfo;
38 import android.content.pm.PackageInfo;
39 import android.os.Handler;
40 import android.os.Looper;
41 import android.os.Message;
42 import android.os.SystemClock;
43 import android.os.UserHandle;
44 import android.util.ArraySet;
45 import android.util.IndentingPrintWriter;
46 import android.util.Log;
47 import android.util.Slog;
48 import android.util.SparseArrayMap;
49 import android.util.TimeUtils;
50 
51 import com.android.internal.annotations.GuardedBy;
52 import com.android.internal.annotations.VisibleForTesting;
53 import com.android.server.LocalServices;
54 import com.android.server.pm.UserManagerInternal;
55 import com.android.server.usage.AppStandbyInternal;
56 import com.android.server.utils.AlarmQueue;
57 
58 import java.util.List;
59 import java.util.Objects;
60 import java.util.function.Consumer;
61 
62 /**
63  * Other half of the IRS. The agent handles the nitty gritty details, interacting directly with
64  * ledgers, carrying out specific events such as wealth reclamation, granting initial balances or
65  * replenishing balances, and tracking ongoing events.
66  */
67 class Agent {
68     private static final String TAG = "TARE-" + Agent.class.getSimpleName();
69     private static final boolean DEBUG = InternalResourceService.DEBUG
70             || Log.isLoggable(TAG, Log.DEBUG);
71 
72     private static final String ALARM_TAG_AFFORDABILITY_CHECK = "*tare.affordability_check*";
73 
74     private final Object mLock;
75     private final Handler mHandler;
76     private final Analyst mAnalyst;
77     private final InternalResourceService mIrs;
78     private final Scribe mScribe;
79 
80     private final AppStandbyInternal mAppStandbyInternal;
81 
82     @GuardedBy("mLock")
83     private final SparseArrayMap<String, SparseArrayMap<String, OngoingEvent>>
84             mCurrentOngoingEvents = new SparseArrayMap<>();
85 
86     /**
87      * Set of {@link ActionAffordabilityNote ActionAffordabilityNotes} keyed by userId-pkgName.
88      *
89      * Note: it would be nice/better to sort by base price since that doesn't change and simply
90      * look at the change in the "insertion" of what would be affordable, but since CTP
91      * is factored into the final price, the sorting order (by modified price) could be different
92      * and that method wouldn't work >:(
93      */
94     @GuardedBy("mLock")
95     private final SparseArrayMap<String, ArraySet<ActionAffordabilityNote>>
96             mActionAffordabilityNotes = new SparseArrayMap<>();
97 
98     /**
99      * Queue to track and manage when apps will cross the closest affordability threshold (in
100      * both directions).
101      */
102     @GuardedBy("mLock")
103     private final BalanceThresholdAlarmQueue mBalanceThresholdAlarmQueue;
104 
105     /**
106      * Check the affordability notes of all apps.
107      */
108     private static final int MSG_CHECK_ALL_AFFORDABILITY = 0;
109     /**
110      * Check the affordability notes of a single app.
111      */
112     private static final int MSG_CHECK_INDIVIDUAL_AFFORDABILITY = 1;
113 
Agent(@onNull InternalResourceService irs, @NonNull Scribe scribe, @NonNull Analyst analyst)114     Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe, @NonNull Analyst analyst) {
115         mLock = irs.getLock();
116         mIrs = irs;
117         mScribe = scribe;
118         mAnalyst = analyst;
119         mHandler = new AgentHandler(TareHandlerThread.get().getLooper());
120         mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class);
121         mBalanceThresholdAlarmQueue = new BalanceThresholdAlarmQueue(
122                 mIrs.getContext(), TareHandlerThread.get().getLooper());
123     }
124 
125     private class TotalDeltaCalculator implements Consumer<OngoingEvent> {
126         private Ledger mLedger;
127         private long mNowElapsed;
128         private long mNow;
129         private long mTotal;
130 
reset(@onNull Ledger ledger, long nowElapsed, long now)131         void reset(@NonNull Ledger ledger, long nowElapsed, long now) {
132             mLedger = ledger;
133             mNowElapsed = nowElapsed;
134             mNow = now;
135             mTotal = 0;
136         }
137 
138         @Override
accept(OngoingEvent ongoingEvent)139         public void accept(OngoingEvent ongoingEvent) {
140             mTotal += getActualDeltaLocked(ongoingEvent, mLedger, mNowElapsed, mNow).price;
141         }
142     }
143 
144     @GuardedBy("mLock")
145     private final TotalDeltaCalculator mTotalDeltaCalculator = new TotalDeltaCalculator();
146 
147     /** Get an app's current balance, factoring in any currently ongoing events. */
148     @GuardedBy("mLock")
getBalanceLocked(final int userId, @NonNull final String pkgName)149     long getBalanceLocked(final int userId, @NonNull final String pkgName) {
150         final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
151         long balance = ledger.getCurrentBalance();
152         SparseArrayMap<String, OngoingEvent> ongoingEvents =
153                 mCurrentOngoingEvents.get(userId, pkgName);
154         if (ongoingEvents != null) {
155             final long nowElapsed = SystemClock.elapsedRealtime();
156             final long now = getCurrentTimeMillis();
157             mTotalDeltaCalculator.reset(ledger, nowElapsed, now);
158             ongoingEvents.forEach(mTotalDeltaCalculator);
159             balance += mTotalDeltaCalculator.mTotal;
160         }
161         return balance;
162     }
163 
164     @GuardedBy("mLock")
isAffordableLocked(long balance, long price, long ctp)165     private boolean isAffordableLocked(long balance, long price, long ctp) {
166         return balance >= price && mScribe.getRemainingConsumableCakesLocked() >= ctp;
167     }
168 
169     @GuardedBy("mLock")
noteInstantaneousEventLocked(final int userId, @NonNull final String pkgName, final int eventId, @Nullable String tag)170     void noteInstantaneousEventLocked(final int userId, @NonNull final String pkgName,
171             final int eventId, @Nullable String tag) {
172         if (mIrs.isSystem(userId, pkgName)) {
173             // Events are free for the system. Don't bother recording them.
174             return;
175         }
176 
177         final long now = getCurrentTimeMillis();
178         final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
179         final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
180 
181         final int eventType = getEventType(eventId);
182         switch (eventType) {
183             case TYPE_ACTION:
184                 final EconomicPolicy.Cost actionCost =
185                         economicPolicy.getCostOfAction(eventId, userId, pkgName);
186 
187                 recordTransactionLocked(userId, pkgName, ledger,
188                         new Ledger.Transaction(now, now, eventId, tag,
189                                 -actionCost.price, actionCost.costToProduce),
190                         true);
191                 break;
192 
193             case TYPE_REWARD:
194                 final EconomicPolicy.Reward reward = economicPolicy.getReward(eventId);
195                 if (reward != null) {
196                     final long rewardSum = ledger.get24HourSum(eventId, now);
197                     final long rewardVal = Math.max(0,
198                             Math.min(reward.maxDailyReward - rewardSum, reward.instantReward));
199                     recordTransactionLocked(userId, pkgName, ledger,
200                             new Ledger.Transaction(now, now, eventId, tag, rewardVal, 0), true);
201                 }
202                 break;
203 
204             default:
205                 Slog.w(TAG, "Unsupported event type: " + eventType);
206         }
207         scheduleBalanceCheckLocked(userId, pkgName);
208     }
209 
210     @GuardedBy("mLock")
noteOngoingEventLocked(final int userId, @NonNull final String pkgName, final int eventId, @Nullable String tag, final long startElapsed)211     void noteOngoingEventLocked(final int userId, @NonNull final String pkgName, final int eventId,
212             @Nullable String tag, final long startElapsed) {
213         noteOngoingEventLocked(userId, pkgName, eventId, tag, startElapsed, true);
214     }
215 
216     @GuardedBy("mLock")
noteOngoingEventLocked(final int userId, @NonNull final String pkgName, final int eventId, @Nullable String tag, final long startElapsed, final boolean updateBalanceCheck)217     private void noteOngoingEventLocked(final int userId, @NonNull final String pkgName,
218             final int eventId, @Nullable String tag, final long startElapsed,
219             final boolean updateBalanceCheck) {
220         if (mIrs.isSystem(userId, pkgName)) {
221             // Events are free for the system. Don't bother recording them.
222             return;
223         }
224 
225         SparseArrayMap<String, OngoingEvent> ongoingEvents =
226                 mCurrentOngoingEvents.get(userId, pkgName);
227         if (ongoingEvents == null) {
228             ongoingEvents = new SparseArrayMap<>();
229             mCurrentOngoingEvents.add(userId, pkgName, ongoingEvents);
230         }
231         OngoingEvent ongoingEvent = ongoingEvents.get(eventId, tag);
232 
233         final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
234         final int eventType = getEventType(eventId);
235         switch (eventType) {
236             case TYPE_ACTION:
237                 final EconomicPolicy.Cost actionCost =
238                         economicPolicy.getCostOfAction(eventId, userId, pkgName);
239 
240                 if (ongoingEvent == null) {
241                     ongoingEvents.add(eventId, tag,
242                             new OngoingEvent(eventId, tag, startElapsed, actionCost));
243                 } else {
244                     ongoingEvent.refCount++;
245                 }
246                 break;
247 
248             case TYPE_REWARD:
249                 final EconomicPolicy.Reward reward = economicPolicy.getReward(eventId);
250                 if (reward != null) {
251                     if (ongoingEvent == null) {
252                         ongoingEvents.add(eventId, tag, new OngoingEvent(
253                                 eventId, tag, startElapsed, reward));
254                     } else {
255                         ongoingEvent.refCount++;
256                     }
257                 }
258                 break;
259 
260             default:
261                 Slog.w(TAG, "Unsupported event type: " + eventType);
262         }
263 
264         if (updateBalanceCheck) {
265             scheduleBalanceCheckLocked(userId, pkgName);
266         }
267     }
268 
269     @GuardedBy("mLock")
onDeviceStateChangedLocked()270     void onDeviceStateChangedLocked() {
271         onPricingChangedLocked();
272     }
273 
274     @GuardedBy("mLock")
onPricingChangedLocked()275     void onPricingChangedLocked() {
276         onAnythingChangedLocked(true);
277     }
278 
279     @GuardedBy("mLock")
onAppStatesChangedLocked(final int userId, @NonNull ArraySet<String> pkgNames)280     void onAppStatesChangedLocked(final int userId, @NonNull ArraySet<String> pkgNames) {
281         final long now = getCurrentTimeMillis();
282         final long nowElapsed = SystemClock.elapsedRealtime();
283         final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
284 
285         for (int i = 0; i < pkgNames.size(); ++i) {
286             final String pkgName = pkgNames.valueAt(i);
287             SparseArrayMap<String, OngoingEvent> ongoingEvents =
288                     mCurrentOngoingEvents.get(userId, pkgName);
289             if (ongoingEvents != null) {
290                 mOngoingEventUpdater.reset(userId, pkgName, now, nowElapsed);
291                 ongoingEvents.forEach(mOngoingEventUpdater);
292                 final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
293                         mActionAffordabilityNotes.get(userId, pkgName);
294                 if (actionAffordabilityNotes != null) {
295                     final int size = actionAffordabilityNotes.size();
296                     final long newBalance =
297                             mScribe.getLedgerLocked(userId, pkgName).getCurrentBalance();
298                     for (int n = 0; n < size; ++n) {
299                         final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n);
300                         note.recalculateCosts(economicPolicy, userId, pkgName);
301                         final boolean isAffordable =
302                                 isAffordableLocked(newBalance,
303                                         note.getCachedModifiedPrice(), note.getCtp());
304                         if (note.isCurrentlyAffordable() != isAffordable) {
305                             note.setNewAffordability(isAffordable);
306                             mIrs.postAffordabilityChanged(userId, pkgName, note);
307                         }
308                     }
309                 }
310                 scheduleBalanceCheckLocked(userId, pkgName);
311             }
312         }
313     }
314 
315     @GuardedBy("mLock")
onAnythingChangedLocked(final boolean updateOngoingEvents)316     private void onAnythingChangedLocked(final boolean updateOngoingEvents) {
317         final long now = getCurrentTimeMillis();
318         final long nowElapsed = SystemClock.elapsedRealtime();
319         final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
320 
321         for (int uIdx = mCurrentOngoingEvents.numMaps() - 1; uIdx >= 0; --uIdx) {
322             final int userId = mCurrentOngoingEvents.keyAt(uIdx);
323 
324             for (int pIdx = mCurrentOngoingEvents.numElementsForKey(userId) - 1; pIdx >= 0;
325                     --pIdx) {
326                 final String pkgName = mCurrentOngoingEvents.keyAt(uIdx, pIdx);
327 
328                 SparseArrayMap<String, OngoingEvent> ongoingEvents =
329                         mCurrentOngoingEvents.valueAt(uIdx, pIdx);
330                 if (ongoingEvents != null) {
331                     if (updateOngoingEvents) {
332                         mOngoingEventUpdater.reset(userId, pkgName, now, nowElapsed);
333                         ongoingEvents.forEach(mOngoingEventUpdater);
334                     }
335                     scheduleBalanceCheckLocked(userId, pkgName);
336                 }
337             }
338         }
339         for (int uIdx = mActionAffordabilityNotes.numMaps() - 1; uIdx >= 0; --uIdx) {
340             final int userId = mActionAffordabilityNotes.keyAt(uIdx);
341 
342             for (int pIdx = mActionAffordabilityNotes.numElementsForKey(userId) - 1; pIdx >= 0;
343                     --pIdx) {
344                 final String pkgName = mActionAffordabilityNotes.keyAt(uIdx, pIdx);
345 
346                 final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
347                         mActionAffordabilityNotes.valueAt(uIdx, pIdx);
348 
349                 if (actionAffordabilityNotes != null) {
350                     final int size = actionAffordabilityNotes.size();
351                     final long newBalance = getBalanceLocked(userId, pkgName);
352                     for (int n = 0; n < size; ++n) {
353                         final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n);
354                         note.recalculateCosts(economicPolicy, userId, pkgName);
355                         final boolean isAffordable =
356                                 isAffordableLocked(newBalance,
357                                         note.getCachedModifiedPrice(), note.getCtp());
358                         if (note.isCurrentlyAffordable() != isAffordable) {
359                             note.setNewAffordability(isAffordable);
360                             mIrs.postAffordabilityChanged(userId, pkgName, note);
361                         }
362                     }
363                 }
364             }
365         }
366     }
367 
368     @GuardedBy("mLock")
stopOngoingActionLocked(final int userId, @NonNull final String pkgName, final int eventId, @Nullable String tag, final long nowElapsed, final long now)369     void stopOngoingActionLocked(final int userId, @NonNull final String pkgName, final int eventId,
370             @Nullable String tag, final long nowElapsed, final long now) {
371         stopOngoingActionLocked(userId, pkgName, eventId, tag, nowElapsed, now, true, true);
372     }
373 
374     /**
375      * @param updateBalanceCheck          Whether to reschedule the affordability/balance
376      *                                    check alarm.
377      * @param notifyOnAffordabilityChange Whether to evaluate the app's ability to afford
378      *                                    registered bills and notify listeners about any changes.
379      */
380     @GuardedBy("mLock")
stopOngoingActionLocked(final int userId, @NonNull final String pkgName, final int eventId, @Nullable String tag, final long nowElapsed, final long now, final boolean updateBalanceCheck, final boolean notifyOnAffordabilityChange)381     private void stopOngoingActionLocked(final int userId, @NonNull final String pkgName,
382             final int eventId, @Nullable String tag, final long nowElapsed, final long now,
383             final boolean updateBalanceCheck, final boolean notifyOnAffordabilityChange) {
384         if (mIrs.isSystem(userId, pkgName)) {
385             // Events are free for the system. Don't bother recording them.
386             return;
387         }
388 
389         final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
390 
391         SparseArrayMap<String, OngoingEvent> ongoingEvents =
392                 mCurrentOngoingEvents.get(userId, pkgName);
393         if (ongoingEvents == null) {
394             // This may occur if TARE goes from disabled to enabled while an event is already
395             // occurring.
396             Slog.w(TAG, "No ongoing transactions for " + appToString(userId, pkgName));
397             return;
398         }
399         final OngoingEvent ongoingEvent = ongoingEvents.get(eventId, tag);
400         if (ongoingEvent == null) {
401             // This may occur if TARE goes from disabled to enabled while an event is already
402             // occurring.
403             Slog.w(TAG, "Nonexistent ongoing transaction "
404                     + eventToString(eventId) + (tag == null ? "" : ":" + tag)
405                     + " for " + appToString(userId, pkgName) + " ended");
406             return;
407         }
408         ongoingEvent.refCount--;
409         if (ongoingEvent.refCount <= 0) {
410             final long startElapsed = ongoingEvent.startTimeElapsed;
411             final long startTime = now - (nowElapsed - startElapsed);
412             final EconomicPolicy.Cost actualDelta =
413                     getActualDeltaLocked(ongoingEvent, ledger, nowElapsed, now);
414             recordTransactionLocked(userId, pkgName, ledger,
415                     new Ledger.Transaction(startTime, now, eventId, tag, actualDelta.price,
416                             actualDelta.costToProduce),
417                     notifyOnAffordabilityChange);
418 
419             ongoingEvents.delete(eventId, tag);
420         }
421         if (updateBalanceCheck) {
422             scheduleBalanceCheckLocked(userId, pkgName);
423         }
424     }
425 
426     @GuardedBy("mLock")
427     @NonNull
getActualDeltaLocked(@onNull OngoingEvent ongoingEvent, @NonNull Ledger ledger, long nowElapsed, long now)428     private EconomicPolicy.Cost getActualDeltaLocked(@NonNull OngoingEvent ongoingEvent,
429             @NonNull Ledger ledger, long nowElapsed, long now) {
430         final long startElapsed = ongoingEvent.startTimeElapsed;
431         final long durationSecs = (nowElapsed - startElapsed) / 1000;
432         final long computedDelta = durationSecs * ongoingEvent.getDeltaPerSec();
433         if (ongoingEvent.reward == null) {
434             return new EconomicPolicy.Cost(
435                     durationSecs * ongoingEvent.getCtpPerSec(), computedDelta);
436         }
437         final long rewardSum = ledger.get24HourSum(ongoingEvent.eventId, now);
438         return new EconomicPolicy.Cost(0,
439                 Math.max(0,
440                         Math.min(ongoingEvent.reward.maxDailyReward - rewardSum, computedDelta)));
441     }
442 
443     @VisibleForTesting
444     @GuardedBy("mLock")
recordTransactionLocked(final int userId, @NonNull final String pkgName, @NonNull Ledger ledger, @NonNull Ledger.Transaction transaction, final boolean notifyOnAffordabilityChange)445     void recordTransactionLocked(final int userId, @NonNull final String pkgName,
446             @NonNull Ledger ledger, @NonNull Ledger.Transaction transaction,
447             final boolean notifyOnAffordabilityChange) {
448         if (!DEBUG && transaction.delta == 0) {
449             // Skip recording transactions with a delta of 0 to save on space.
450             return;
451         }
452         if (mIrs.isSystem(userId, pkgName)) {
453             Slog.wtfStack(TAG,
454                     "Tried to adjust system balance for " + appToString(userId, pkgName));
455             return;
456         }
457         final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
458         final long originalBalance = ledger.getCurrentBalance();
459         if (transaction.delta > 0
460                 && originalBalance + transaction.delta > economicPolicy.getMaxSatiatedBalance()) {
461             // Set lower bound at 0 so we don't accidentally take away credits when we were trying
462             // to _give_ the app credits.
463             final long newDelta =
464                     Math.max(0, economicPolicy.getMaxSatiatedBalance() - originalBalance);
465             Slog.i(TAG, "Would result in becoming too rich. Decreasing transaction "
466                     + eventToString(transaction.eventId)
467                     + (transaction.tag == null ? "" : ":" + transaction.tag)
468                     + " for " + appToString(userId, pkgName)
469                     + " by " + cakeToString(transaction.delta - newDelta));
470             transaction = new Ledger.Transaction(
471                     transaction.startTimeMs, transaction.endTimeMs,
472                     transaction.eventId, transaction.tag, newDelta, transaction.ctp);
473         }
474         ledger.recordTransaction(transaction);
475         mScribe.adjustRemainingConsumableCakesLocked(-transaction.ctp);
476         mAnalyst.noteTransaction(transaction);
477         if (transaction.delta != 0 && notifyOnAffordabilityChange) {
478             final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
479                     mActionAffordabilityNotes.get(userId, pkgName);
480             if (actionAffordabilityNotes != null) {
481                 final long newBalance = ledger.getCurrentBalance();
482                 for (int i = 0; i < actionAffordabilityNotes.size(); ++i) {
483                     final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i);
484                     final boolean isAffordable =
485                             isAffordableLocked(newBalance,
486                                     note.getCachedModifiedPrice(), note.getCtp());
487                     if (note.isCurrentlyAffordable() != isAffordable) {
488                         note.setNewAffordability(isAffordable);
489                         mIrs.postAffordabilityChanged(userId, pkgName, note);
490                     }
491                 }
492             }
493         }
494         if (transaction.ctp != 0) {
495             mHandler.sendEmptyMessage(MSG_CHECK_ALL_AFFORDABILITY);
496             mIrs.maybePerformQuantitativeEasingLocked();
497         }
498     }
499 
500     /**
501      * Reclaim a percentage of unused ARCs from every app that hasn't been used recently. The
502      * reclamation will not reduce an app's balance below its minimum balance as dictated by
503      * {@code scaleMinBalance}.
504      *
505      * @param percentage      A value between 0 and 1 to indicate how much of the unused balance
506      *                        should be reclaimed.
507      * @param minUnusedTimeMs The minimum amount of time (in milliseconds) that must have
508      *                        transpired since the last user usage event before we will consider
509      *                        reclaiming ARCs from the app.
510      * @param scaleMinBalance Whether or not to used the scaled minimum app balance. If false,
511      *                        this will use the constant min balance floor given by
512      *                        {@link EconomicPolicy#getMinSatiatedBalance(int, String)}. If true,
513      *                        this will use the scaled balance given by
514      *                        {@link InternalResourceService#getMinBalanceLocked(int, String)}.
515      */
516     @GuardedBy("mLock")
reclaimUnusedAssetsLocked(double percentage, long minUnusedTimeMs, boolean scaleMinBalance)517     void reclaimUnusedAssetsLocked(double percentage, long minUnusedTimeMs,
518             boolean scaleMinBalance) {
519         final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
520         final SparseArrayMap<String, Ledger> ledgers = mScribe.getLedgersLocked();
521         final long now = getCurrentTimeMillis();
522         for (int u = 0; u < ledgers.numMaps(); ++u) {
523             final int userId = ledgers.keyAt(u);
524             for (int p = 0; p < ledgers.numElementsForKey(userId); ++p) {
525                 final Ledger ledger = ledgers.valueAt(u, p);
526                 final long curBalance = ledger.getCurrentBalance();
527                 if (curBalance <= 0) {
528                     continue;
529                 }
530                 final String pkgName = ledgers.keyAt(u, p);
531                 // AppStandby only counts elapsed time for things like this
532                 // TODO: should we use clock time instead?
533                 final long timeSinceLastUsedMs =
534                         mAppStandbyInternal.getTimeSinceLastUsedByUser(pkgName, userId);
535                 if (timeSinceLastUsedMs >= minUnusedTimeMs) {
536                     final long minBalance;
537                     if (!scaleMinBalance) {
538                         // Use a constant floor instead of the scaled floor from the IRS.
539                         minBalance = economicPolicy.getMinSatiatedBalance(userId, pkgName);
540                     } else {
541                         minBalance = mIrs.getMinBalanceLocked(userId, pkgName);
542                     }
543                     long toReclaim = (long) (curBalance * percentage);
544                     if (curBalance - toReclaim < minBalance) {
545                         toReclaim = curBalance - minBalance;
546                     }
547                     if (toReclaim > 0) {
548                         if (DEBUG) {
549                             Slog.i(TAG, "Reclaiming unused wealth! Taking " + toReclaim
550                                     + " from " + appToString(userId, pkgName));
551                         }
552 
553                         recordTransactionLocked(userId, pkgName, ledger,
554                                 new Ledger.Transaction(now, now, REGULATION_WEALTH_RECLAMATION,
555                                         null, -toReclaim, 0),
556                                 true);
557                     }
558                 }
559             }
560         }
561     }
562 
563     /**
564      * Reclaim a percentage of unused ARCs from an app that was just removed from an exemption list.
565      * The amount reclaimed will depend on how recently the app was used. The reclamation will not
566      * reduce an app's balance below its current minimum balance.
567      */
568     @GuardedBy("mLock")
onAppUnexemptedLocked(final int userId, @NonNull final String pkgName)569     void onAppUnexemptedLocked(final int userId, @NonNull final String pkgName) {
570         final long curBalance = getBalanceLocked(userId, pkgName);
571         final long minBalance = mIrs.getMinBalanceLocked(userId, pkgName);
572         if (curBalance <= minBalance) {
573             return;
574         }
575         // AppStandby only counts elapsed time for things like this
576         // TODO: should we use clock time instead?
577         final long timeSinceLastUsedMs =
578                 mAppStandbyInternal.getTimeSinceLastUsedByUser(pkgName, userId);
579         // The app is no longer exempted. We should take away some of credits so it's more in line
580         // with other non-exempt apps. However, don't take away as many credits if the app was used
581         // recently.
582         final double percentageToReclaim;
583         if (timeSinceLastUsedMs < DAY_IN_MILLIS) {
584             percentageToReclaim = .25;
585         } else if (timeSinceLastUsedMs < 2 * DAY_IN_MILLIS) {
586             percentageToReclaim = .5;
587         } else if (timeSinceLastUsedMs < 3 * DAY_IN_MILLIS) {
588             percentageToReclaim = .75;
589         } else {
590             percentageToReclaim = 1;
591         }
592         final long overage = curBalance - minBalance;
593         final long toReclaim = (long) (overage * percentageToReclaim);
594         if (toReclaim > 0) {
595             if (DEBUG) {
596                 Slog.i(TAG, "Reclaiming bonus wealth! Taking " + toReclaim
597                         + " from " + appToString(userId, pkgName));
598             }
599 
600             final long now = getCurrentTimeMillis();
601             final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
602             recordTransactionLocked(userId, pkgName, ledger,
603                     new Ledger.Transaction(now, now, REGULATION_DEMOTION, null, -toReclaim, 0),
604                     true);
605         }
606     }
607 
608     /** Returns true if an app should be given credits in the general distributions. */
shouldGiveCredits(@onNull PackageInfo packageInfo)609     private boolean shouldGiveCredits(@NonNull PackageInfo packageInfo) {
610         final ApplicationInfo applicationInfo = packageInfo.applicationInfo;
611         // Skip apps that wouldn't be doing any work. Giving them ARCs would be wasteful.
612         if (applicationInfo == null || !applicationInfo.hasCode()) {
613             return false;
614         }
615         final int userId = UserHandle.getUserId(packageInfo.applicationInfo.uid);
616         // No point allocating ARCs to the system. It can do whatever it wants.
617         return !mIrs.isSystem(userId, packageInfo.packageName);
618     }
619 
onCreditSupplyChanged()620     void onCreditSupplyChanged() {
621         mHandler.sendEmptyMessage(MSG_CHECK_ALL_AFFORDABILITY);
622     }
623 
624     @GuardedBy("mLock")
distributeBasicIncomeLocked(int batteryLevel)625     void distributeBasicIncomeLocked(int batteryLevel) {
626         List<PackageInfo> pkgs = mIrs.getInstalledPackages();
627 
628         final long now = getCurrentTimeMillis();
629         for (int i = 0; i < pkgs.size(); ++i) {
630             final PackageInfo pkgInfo = pkgs.get(i);
631             if (!shouldGiveCredits(pkgInfo)) {
632                 continue;
633             }
634             final int userId = UserHandle.getUserId(pkgInfo.applicationInfo.uid);
635             final String pkgName = pkgInfo.packageName;
636             final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
637             final long minBalance = mIrs.getMinBalanceLocked(userId, pkgName);
638             final double perc = batteryLevel / 100d;
639             // TODO: maybe don't give credits to bankrupt apps until battery level >= 50%
640             final long shortfall = minBalance - ledger.getCurrentBalance();
641             if (shortfall > 0) {
642                 recordTransactionLocked(userId, pkgName, ledger,
643                         new Ledger.Transaction(now, now, REGULATION_BASIC_INCOME,
644                                 null, (long) (perc * shortfall), 0), true);
645             }
646         }
647     }
648 
649     /** Give each app an initial balance. */
650     @GuardedBy("mLock")
grantBirthrightsLocked()651     void grantBirthrightsLocked() {
652         UserManagerInternal userManagerInternal =
653                 LocalServices.getService(UserManagerInternal.class);
654         final int[] userIds = userManagerInternal.getUserIds();
655         for (int userId : userIds) {
656             grantBirthrightsLocked(userId);
657         }
658     }
659 
660     @GuardedBy("mLock")
grantBirthrightsLocked(final int userId)661     void grantBirthrightsLocked(final int userId) {
662         final List<PackageInfo> pkgs = mIrs.getInstalledPackages(userId);
663         final long now = getCurrentTimeMillis();
664 
665         for (int i = 0; i < pkgs.size(); ++i) {
666             final PackageInfo packageInfo = pkgs.get(i);
667             if (!shouldGiveCredits(packageInfo)) {
668                 continue;
669             }
670             final String pkgName = packageInfo.packageName;
671             final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
672             if (ledger.getCurrentBalance() > 0) {
673                 // App already got credits somehow. Move along.
674                 Slog.wtf(TAG, "App " + pkgName + " had credits before economy was set up");
675                 continue;
676             }
677 
678             recordTransactionLocked(userId, pkgName, ledger,
679                     new Ledger.Transaction(now, now, REGULATION_BIRTHRIGHT, null,
680                             mIrs.getMinBalanceLocked(userId, pkgName), 0),
681                     true);
682         }
683     }
684 
685     @GuardedBy("mLock")
grantBirthrightLocked(final int userId, @NonNull final String pkgName)686     void grantBirthrightLocked(final int userId, @NonNull final String pkgName) {
687         final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
688         if (ledger.getCurrentBalance() > 0) {
689             Slog.wtf(TAG, "App " + pkgName + " had credits as soon as it was installed");
690             // App already got credits somehow. Move along.
691             return;
692         }
693 
694         final long now = getCurrentTimeMillis();
695 
696         recordTransactionLocked(userId, pkgName, ledger,
697                 new Ledger.Transaction(now, now, REGULATION_BIRTHRIGHT, null,
698                         mIrs.getMinBalanceLocked(userId, pkgName), 0), true);
699     }
700 
701     @GuardedBy("mLock")
onAppExemptedLocked(final int userId, @NonNull final String pkgName)702     void onAppExemptedLocked(final int userId, @NonNull final String pkgName) {
703         final long minBalance = mIrs.getMinBalanceLocked(userId, pkgName);
704         final long missing = minBalance - getBalanceLocked(userId, pkgName);
705         if (missing <= 0) {
706             return;
707         }
708 
709         final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
710         final long now = getCurrentTimeMillis();
711 
712         recordTransactionLocked(userId, pkgName, ledger,
713                 new Ledger.Transaction(now, now, REGULATION_PROMOTION, null, missing, 0), true);
714     }
715 
716     @GuardedBy("mLock")
onPackageRemovedLocked(final int userId, @NonNull final String pkgName)717     void onPackageRemovedLocked(final int userId, @NonNull final String pkgName) {
718         reclaimAssetsLocked(userId, pkgName);
719         mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
720     }
721 
722     /**
723      * Reclaims any ARCs granted to the app, making them available to other apps. Also deletes the
724      * app's ledger and stops any ongoing event tracking.
725      */
726     @GuardedBy("mLock")
reclaimAssetsLocked(final int userId, @NonNull final String pkgName)727     private void reclaimAssetsLocked(final int userId, @NonNull final String pkgName) {
728         final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
729         if (ledger.getCurrentBalance() != 0) {
730             mScribe.adjustRemainingConsumableCakesLocked(-ledger.getCurrentBalance());
731         }
732         mScribe.discardLedgerLocked(userId, pkgName);
733         mCurrentOngoingEvents.delete(userId, pkgName);
734     }
735 
736     @GuardedBy("mLock")
onUserRemovedLocked(final int userId, @NonNull final List<String> pkgNames)737     void onUserRemovedLocked(final int userId, @NonNull final List<String> pkgNames) {
738         reclaimAssetsLocked(userId, pkgNames);
739         mBalanceThresholdAlarmQueue.removeAlarmsForUserId(userId);
740     }
741 
742     @GuardedBy("mLock")
reclaimAssetsLocked(final int userId, @NonNull final List<String> pkgNames)743     private void reclaimAssetsLocked(final int userId, @NonNull final List<String> pkgNames) {
744         for (int i = 0; i < pkgNames.size(); ++i) {
745             reclaimAssetsLocked(userId, pkgNames.get(i));
746         }
747     }
748 
749     @VisibleForTesting
750     static class TrendCalculator implements Consumer<OngoingEvent> {
751         static final long WILL_NOT_CROSS_THRESHOLD = -1;
752 
753         private long mCurBalance;
754         private long mRemainingConsumableCredits;
755         /**
756          * The maximum change in credits per second towards the upper threshold
757          * {@link #mUpperThreshold}. A value of 0 means the current ongoing events will never
758          * result in the app crossing the upper threshold.
759          */
760         private long mMaxDeltaPerSecToUpperThreshold;
761         /**
762          * The maximum change in credits per second towards the lower threshold
763          * {@link #mLowerThreshold}. A value of 0 means the current ongoing events will never
764          * result in the app crossing the lower threshold.
765          */
766         private long mMaxDeltaPerSecToLowerThreshold;
767         /**
768          * The maximum change in credits per second towards the highest CTP threshold below the
769          * remaining consumable credits (cached in {@link #mCtpThreshold}). A value of 0 means
770          * the current ongoing events will never result in the app crossing the lower threshold.
771          */
772         private long mMaxDeltaPerSecToCtpThreshold;
773         private long mUpperThreshold;
774         private long mLowerThreshold;
775         private long mCtpThreshold;
776 
reset(long curBalance, long remainingConsumableCredits, @Nullable ArraySet<ActionAffordabilityNote> actionAffordabilityNotes)777         void reset(long curBalance, long remainingConsumableCredits,
778                 @Nullable ArraySet<ActionAffordabilityNote> actionAffordabilityNotes) {
779             mCurBalance = curBalance;
780             mRemainingConsumableCredits = remainingConsumableCredits;
781             mMaxDeltaPerSecToUpperThreshold = mMaxDeltaPerSecToLowerThreshold = 0;
782             mMaxDeltaPerSecToCtpThreshold = 0;
783             mUpperThreshold = Long.MIN_VALUE;
784             mLowerThreshold = Long.MAX_VALUE;
785             mCtpThreshold = 0;
786             if (actionAffordabilityNotes != null) {
787                 for (int i = 0; i < actionAffordabilityNotes.size(); ++i) {
788                     final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i);
789                     final long price = note.getCachedModifiedPrice();
790                     if (price <= mCurBalance) {
791                         mLowerThreshold = (mLowerThreshold == Long.MAX_VALUE)
792                                 ? price : Math.max(mLowerThreshold, price);
793                     } else {
794                         mUpperThreshold = (mUpperThreshold == Long.MIN_VALUE)
795                                 ? price : Math.min(mUpperThreshold, price);
796                     }
797                     final long ctp = note.getCtp();
798                     if (ctp <= mRemainingConsumableCredits) {
799                         mCtpThreshold = Math.max(mCtpThreshold, ctp);
800                     }
801                 }
802             }
803         }
804 
805         /**
806          * Returns the amount of time (in millisecond) it will take for the app to cross the next
807          * lowest action affordability note (compared to its current balance) based on current
808          * ongoing events.
809          * Returns {@link #WILL_NOT_CROSS_THRESHOLD} if the app will never cross the lowest
810          * threshold.
811          */
getTimeToCrossLowerThresholdMs()812         long getTimeToCrossLowerThresholdMs() {
813             if (mMaxDeltaPerSecToLowerThreshold == 0 && mMaxDeltaPerSecToCtpThreshold == 0) {
814                 // Will never cross lower threshold based on current events.
815                 return WILL_NOT_CROSS_THRESHOLD;
816             }
817             long minSeconds = Long.MAX_VALUE;
818             if (mMaxDeltaPerSecToLowerThreshold != 0) {
819                 // deltaPerSec is a negative value, so do threshold-balance to cancel out the
820                 // negative.
821                 minSeconds = (mLowerThreshold - mCurBalance) / mMaxDeltaPerSecToLowerThreshold;
822             }
823             if (mMaxDeltaPerSecToCtpThreshold != 0) {
824                 minSeconds = Math.min(minSeconds,
825                         // deltaPerSec is a negative value, so do threshold-balance to cancel
826                         // out the negative.
827                         (mCtpThreshold - mRemainingConsumableCredits)
828                                 / mMaxDeltaPerSecToCtpThreshold);
829             }
830             return minSeconds * 1000;
831         }
832 
833         /**
834          * Returns the amount of time (in millisecond) it will take for the app to cross the next
835          * highest action affordability note (compared to its current balance) based on current
836          * ongoing events.
837          * Returns {@link #WILL_NOT_CROSS_THRESHOLD} if the app will never cross the upper
838          * threshold.
839          */
getTimeToCrossUpperThresholdMs()840         long getTimeToCrossUpperThresholdMs() {
841             if (mMaxDeltaPerSecToUpperThreshold == 0) {
842                 // Will never cross upper threshold based on current events.
843                 return WILL_NOT_CROSS_THRESHOLD;
844             }
845             final long minSeconds =
846                     (mUpperThreshold - mCurBalance) / mMaxDeltaPerSecToUpperThreshold;
847             return minSeconds * 1000;
848         }
849 
850         @Override
accept(OngoingEvent ongoingEvent)851         public void accept(OngoingEvent ongoingEvent) {
852             final long deltaPerSec = ongoingEvent.getDeltaPerSec();
853             if (mCurBalance >= mLowerThreshold && deltaPerSec < 0) {
854                 mMaxDeltaPerSecToLowerThreshold += deltaPerSec;
855             } else if (mCurBalance < mUpperThreshold && deltaPerSec > 0) {
856                 mMaxDeltaPerSecToUpperThreshold += deltaPerSec;
857             }
858             final long ctpPerSec = ongoingEvent.getCtpPerSec();
859             if (mRemainingConsumableCredits >= mCtpThreshold && deltaPerSec < 0) {
860                 mMaxDeltaPerSecToCtpThreshold -= ctpPerSec;
861             }
862         }
863     }
864 
865     @GuardedBy("mLock")
866     private final TrendCalculator mTrendCalculator = new TrendCalculator();
867 
868     @GuardedBy("mLock")
scheduleBalanceCheckLocked(final int userId, @NonNull final String pkgName)869     private void scheduleBalanceCheckLocked(final int userId, @NonNull final String pkgName) {
870         SparseArrayMap<String, OngoingEvent> ongoingEvents =
871                 mCurrentOngoingEvents.get(userId, pkgName);
872         if (ongoingEvents == null) {
873             // No ongoing transactions. No reason to schedule
874             mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
875             return;
876         }
877         mTrendCalculator.reset(getBalanceLocked(userId, pkgName),
878                 mScribe.getRemainingConsumableCakesLocked(),
879                 mActionAffordabilityNotes.get(userId, pkgName));
880         ongoingEvents.forEach(mTrendCalculator);
881         final long lowerTimeMs = mTrendCalculator.getTimeToCrossLowerThresholdMs();
882         final long upperTimeMs = mTrendCalculator.getTimeToCrossUpperThresholdMs();
883         final long timeToThresholdMs;
884         if (lowerTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) {
885             if (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) {
886                 // Will never cross a threshold based on current events.
887                 mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
888                 return;
889             }
890             timeToThresholdMs = upperTimeMs;
891         } else {
892             timeToThresholdMs = (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD)
893                     ? lowerTimeMs : Math.min(lowerTimeMs, upperTimeMs);
894         }
895         mBalanceThresholdAlarmQueue.addAlarm(new Package(userId, pkgName),
896                 SystemClock.elapsedRealtime() + timeToThresholdMs);
897     }
898 
899     @GuardedBy("mLock")
tearDownLocked()900     void tearDownLocked() {
901         mCurrentOngoingEvents.clear();
902         mBalanceThresholdAlarmQueue.removeAllAlarms();
903     }
904 
905     @VisibleForTesting
906     static class OngoingEvent {
907         public final long startTimeElapsed;
908         public final int eventId;
909         @Nullable
910         public final String tag;
911         @Nullable
912         public final EconomicPolicy.Reward reward;
913         @Nullable
914         public final EconomicPolicy.Cost actionCost;
915         public int refCount;
916 
OngoingEvent(int eventId, @Nullable String tag, long startTimeElapsed, @NonNull EconomicPolicy.Reward reward)917         OngoingEvent(int eventId, @Nullable String tag, long startTimeElapsed,
918                 @NonNull EconomicPolicy.Reward reward) {
919             this.startTimeElapsed = startTimeElapsed;
920             this.eventId = eventId;
921             this.tag = tag;
922             this.reward = reward;
923             this.actionCost = null;
924             refCount = 1;
925         }
926 
OngoingEvent(int eventId, @Nullable String tag, long startTimeElapsed, @NonNull EconomicPolicy.Cost actionCost)927         OngoingEvent(int eventId, @Nullable String tag, long startTimeElapsed,
928                 @NonNull EconomicPolicy.Cost actionCost) {
929             this.startTimeElapsed = startTimeElapsed;
930             this.eventId = eventId;
931             this.tag = tag;
932             this.reward = null;
933             this.actionCost = actionCost;
934             refCount = 1;
935         }
936 
getDeltaPerSec()937         long getDeltaPerSec() {
938             if (actionCost != null) {
939                 return -actionCost.price;
940             }
941             if (reward != null) {
942                 return reward.ongoingRewardPerSecond;
943             }
944             Slog.wtfStack(TAG, "No action or reward in ongoing event?!??!");
945             return 0;
946         }
947 
getCtpPerSec()948         long getCtpPerSec() {
949             if (actionCost != null) {
950                 return actionCost.costToProduce;
951             }
952             return 0;
953         }
954     }
955 
956     private class OngoingEventUpdater implements Consumer<OngoingEvent> {
957         private int mUserId;
958         private String mPkgName;
959         private long mNow;
960         private long mNowElapsed;
961 
reset(int userId, String pkgName, long now, long nowElapsed)962         private void reset(int userId, String pkgName, long now, long nowElapsed) {
963             mUserId = userId;
964             mPkgName = pkgName;
965             mNow = now;
966             mNowElapsed = nowElapsed;
967         }
968 
969         @Override
accept(OngoingEvent ongoingEvent)970         public void accept(OngoingEvent ongoingEvent) {
971             // Disable balance check & affordability notifications here because
972             // we're in the middle of updating ongoing action costs/prices and
973             // sending out notifications or rescheduling the balance check alarm
974             // would be a waste since we'll have to redo them again after all of
975             // our internal state is updated.
976             final boolean updateBalanceCheck = false;
977             stopOngoingActionLocked(mUserId, mPkgName, ongoingEvent.eventId, ongoingEvent.tag,
978                     mNowElapsed, mNow, updateBalanceCheck, /* notifyOnAffordabilityChange */ false);
979             noteOngoingEventLocked(mUserId, mPkgName, ongoingEvent.eventId, ongoingEvent.tag,
980                     mNowElapsed, updateBalanceCheck);
981         }
982     }
983 
984     private final OngoingEventUpdater mOngoingEventUpdater = new OngoingEventUpdater();
985 
986     private static final class Package {
987         public final String packageName;
988         public final int userId;
989 
Package(int userId, String packageName)990         Package(int userId, String packageName) {
991             this.userId = userId;
992             this.packageName = packageName;
993         }
994 
995         @Override
toString()996         public String toString() {
997             return appToString(userId, packageName);
998         }
999 
1000         @Override
equals(Object obj)1001         public boolean equals(Object obj) {
1002             if (obj == null) {
1003                 return false;
1004             }
1005             if (this == obj) {
1006                 return true;
1007             }
1008             if (obj instanceof Package) {
1009                 Package other = (Package) obj;
1010                 return userId == other.userId && Objects.equals(packageName, other.packageName);
1011             }
1012             return false;
1013         }
1014 
1015         @Override
hashCode()1016         public int hashCode() {
1017             return packageName.hashCode() + userId;
1018         }
1019     }
1020 
1021     /** Track when apps will cross the closest affordability threshold (in both directions). */
1022     private class BalanceThresholdAlarmQueue extends AlarmQueue<Package> {
BalanceThresholdAlarmQueue(Context context, Looper looper)1023         private BalanceThresholdAlarmQueue(Context context, Looper looper) {
1024             super(context, looper, ALARM_TAG_AFFORDABILITY_CHECK, "Affordability check", true,
1025                     15_000L);
1026         }
1027 
1028         @Override
isForUser(@onNull Package key, int userId)1029         protected boolean isForUser(@NonNull Package key, int userId) {
1030             return key.userId == userId;
1031         }
1032 
1033         @Override
processExpiredAlarms(@onNull ArraySet<Package> expired)1034         protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
1035             for (int i = 0; i < expired.size(); ++i) {
1036                 Package p = expired.valueAt(i);
1037                 mHandler.obtainMessage(
1038                         MSG_CHECK_INDIVIDUAL_AFFORDABILITY, p.userId, 0, p.packageName)
1039                         .sendToTarget();
1040             }
1041         }
1042     }
1043 
1044     @GuardedBy("mLock")
registerAffordabilityChangeListenerLocked(int userId, @NonNull String pkgName, @NonNull EconomyManagerInternal.AffordabilityChangeListener listener, @NonNull EconomyManagerInternal.ActionBill bill)1045     public void registerAffordabilityChangeListenerLocked(int userId, @NonNull String pkgName,
1046             @NonNull EconomyManagerInternal.AffordabilityChangeListener listener,
1047             @NonNull EconomyManagerInternal.ActionBill bill) {
1048         ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
1049                 mActionAffordabilityNotes.get(userId, pkgName);
1050         if (actionAffordabilityNotes == null) {
1051             actionAffordabilityNotes = new ArraySet<>();
1052             mActionAffordabilityNotes.add(userId, pkgName, actionAffordabilityNotes);
1053         }
1054         final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
1055         final ActionAffordabilityNote note =
1056                 new ActionAffordabilityNote(bill, listener, economicPolicy);
1057         if (actionAffordabilityNotes.add(note)) {
1058             if (!mIrs.isEnabled()) {
1059                 // When TARE isn't enabled, we always say something is affordable. We also don't
1060                 // want to silently drop affordability change listeners in case TARE becomes enabled
1061                 // because then clients will be in an ambiguous state.
1062                 note.setNewAffordability(true);
1063                 return;
1064             }
1065             note.recalculateCosts(economicPolicy, userId, pkgName);
1066             note.setNewAffordability(
1067                     isAffordableLocked(getBalanceLocked(userId, pkgName),
1068                             note.getCachedModifiedPrice(), note.getCtp()));
1069             mIrs.postAffordabilityChanged(userId, pkgName, note);
1070             // Update ongoing alarm
1071             scheduleBalanceCheckLocked(userId, pkgName);
1072         }
1073     }
1074 
1075     @GuardedBy("mLock")
unregisterAffordabilityChangeListenerLocked(int userId, @NonNull String pkgName, @NonNull EconomyManagerInternal.AffordabilityChangeListener listener, @NonNull EconomyManagerInternal.ActionBill bill)1076     public void unregisterAffordabilityChangeListenerLocked(int userId, @NonNull String pkgName,
1077             @NonNull EconomyManagerInternal.AffordabilityChangeListener listener,
1078             @NonNull EconomyManagerInternal.ActionBill bill) {
1079         final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
1080                 mActionAffordabilityNotes.get(userId, pkgName);
1081         if (actionAffordabilityNotes != null) {
1082             final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
1083             final ActionAffordabilityNote note =
1084                     new ActionAffordabilityNote(bill, listener, economicPolicy);
1085             if (actionAffordabilityNotes.remove(note)) {
1086                 // Update ongoing alarm
1087                 scheduleBalanceCheckLocked(userId, pkgName);
1088             }
1089         }
1090     }
1091 
1092     static final class ActionAffordabilityNote {
1093         private final EconomyManagerInternal.ActionBill mActionBill;
1094         private final EconomyManagerInternal.AffordabilityChangeListener mListener;
1095         private long mCtp;
1096         private long mModifiedPrice;
1097         private boolean mIsAffordable;
1098 
1099         @VisibleForTesting
ActionAffordabilityNote(@onNull EconomyManagerInternal.ActionBill bill, @NonNull EconomyManagerInternal.AffordabilityChangeListener listener, @NonNull EconomicPolicy economicPolicy)1100         ActionAffordabilityNote(@NonNull EconomyManagerInternal.ActionBill bill,
1101                 @NonNull EconomyManagerInternal.AffordabilityChangeListener listener,
1102                 @NonNull EconomicPolicy economicPolicy) {
1103             mActionBill = bill;
1104             final List<EconomyManagerInternal.AnticipatedAction> anticipatedActions =
1105                     bill.getAnticipatedActions();
1106             for (int i = 0; i < anticipatedActions.size(); ++i) {
1107                 final EconomyManagerInternal.AnticipatedAction aa = anticipatedActions.get(i);
1108                 final EconomicPolicy.Action action = economicPolicy.getAction(aa.actionId);
1109                 if (action == null) {
1110                     throw new IllegalArgumentException("Invalid action id: " + aa.actionId);
1111                 }
1112             }
1113             mListener = listener;
1114         }
1115 
1116         @NonNull
getActionBill()1117         EconomyManagerInternal.ActionBill getActionBill() {
1118             return mActionBill;
1119         }
1120 
1121         @NonNull
getListener()1122         EconomyManagerInternal.AffordabilityChangeListener getListener() {
1123             return mListener;
1124         }
1125 
getCachedModifiedPrice()1126         private long getCachedModifiedPrice() {
1127             return mModifiedPrice;
1128         }
1129 
getCtp()1130         private long getCtp() {
1131             return mCtp;
1132         }
1133 
1134         @VisibleForTesting
recalculateCosts(@onNull EconomicPolicy economicPolicy, int userId, @NonNull String pkgName)1135         void recalculateCosts(@NonNull EconomicPolicy economicPolicy,
1136                 int userId, @NonNull String pkgName) {
1137             long modifiedPrice = 0;
1138             long ctp = 0;
1139             final List<EconomyManagerInternal.AnticipatedAction> anticipatedActions =
1140                     mActionBill.getAnticipatedActions();
1141             for (int i = 0; i < anticipatedActions.size(); ++i) {
1142                 final EconomyManagerInternal.AnticipatedAction aa = anticipatedActions.get(i);
1143 
1144                 final EconomicPolicy.Cost actionCost =
1145                         economicPolicy.getCostOfAction(aa.actionId, userId, pkgName);
1146                 modifiedPrice += actionCost.price * aa.numInstantaneousCalls
1147                         + actionCost.price * (aa.ongoingDurationMs / 1000);
1148                 ctp += actionCost.costToProduce * aa.numInstantaneousCalls
1149                         + actionCost.costToProduce * (aa.ongoingDurationMs / 1000);
1150             }
1151             mModifiedPrice = modifiedPrice;
1152             mCtp = ctp;
1153         }
1154 
isCurrentlyAffordable()1155         boolean isCurrentlyAffordable() {
1156             return mIsAffordable;
1157         }
1158 
setNewAffordability(boolean isAffordable)1159         private void setNewAffordability(boolean isAffordable) {
1160             mIsAffordable = isAffordable;
1161         }
1162 
1163         @Override
equals(Object o)1164         public boolean equals(Object o) {
1165             if (this == o) return true;
1166             if (!(o instanceof ActionAffordabilityNote)) return false;
1167             ActionAffordabilityNote other = (ActionAffordabilityNote) o;
1168             return mActionBill.equals(other.mActionBill)
1169                     && mListener.equals(other.mListener);
1170         }
1171 
1172         @Override
hashCode()1173         public int hashCode() {
1174             int hash = 0;
1175             hash = 31 * hash + Objects.hash(mListener);
1176             hash = 31 * hash + mActionBill.hashCode();
1177             return hash;
1178         }
1179     }
1180 
1181     private final class AgentHandler extends Handler {
AgentHandler(Looper looper)1182         AgentHandler(Looper looper) {
1183             super(looper);
1184         }
1185 
1186         @Override
handleMessage(Message msg)1187         public void handleMessage(Message msg) {
1188             switch (msg.what) {
1189                 case MSG_CHECK_ALL_AFFORDABILITY: {
1190                     synchronized (mLock) {
1191                         removeMessages(MSG_CHECK_ALL_AFFORDABILITY);
1192                         onAnythingChangedLocked(false);
1193                     }
1194                 }
1195                 break;
1196 
1197                 case MSG_CHECK_INDIVIDUAL_AFFORDABILITY: {
1198                     final int userId = msg.arg1;
1199                     final String pkgName = (String) msg.obj;
1200                     synchronized (mLock) {
1201                         final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
1202                                 mActionAffordabilityNotes.get(userId, pkgName);
1203                         if (actionAffordabilityNotes != null
1204                                 && actionAffordabilityNotes.size() > 0) {
1205                             final long newBalance = getBalanceLocked(userId, pkgName);
1206 
1207                             for (int i = 0; i < actionAffordabilityNotes.size(); ++i) {
1208                                 final ActionAffordabilityNote note =
1209                                         actionAffordabilityNotes.valueAt(i);
1210                                 final boolean isAffordable = isAffordableLocked(
1211                                         newBalance, note.getCachedModifiedPrice(), note.getCtp());
1212                                 if (note.isCurrentlyAffordable() != isAffordable) {
1213                                     note.setNewAffordability(isAffordable);
1214                                     mIrs.postAffordabilityChanged(userId, pkgName, note);
1215                                 }
1216                             }
1217                         }
1218                         scheduleBalanceCheckLocked(userId, pkgName);
1219                     }
1220                 }
1221                 break;
1222             }
1223         }
1224     }
1225 
1226     @GuardedBy("mLock")
dumpLocked(IndentingPrintWriter pw)1227     void dumpLocked(IndentingPrintWriter pw) {
1228         pw.println();
1229         mBalanceThresholdAlarmQueue.dump(pw);
1230 
1231         pw.println();
1232         pw.println("Ongoing events:");
1233         pw.increaseIndent();
1234         boolean printedEvents = false;
1235         final long nowElapsed = SystemClock.elapsedRealtime();
1236         for (int u = mCurrentOngoingEvents.numMaps() - 1; u >= 0; --u) {
1237             final int userId = mCurrentOngoingEvents.keyAt(u);
1238             for (int p = mCurrentOngoingEvents.numElementsForKey(userId) - 1; p >= 0; --p) {
1239                 final String pkgName = mCurrentOngoingEvents.keyAt(u, p);
1240                 final SparseArrayMap<String, OngoingEvent> ongoingEvents =
1241                         mCurrentOngoingEvents.get(userId, pkgName);
1242 
1243                 boolean printedApp = false;
1244 
1245                 for (int e = ongoingEvents.numMaps() - 1; e >= 0; --e) {
1246                     final int eventId = ongoingEvents.keyAt(e);
1247                     for (int t = ongoingEvents.numElementsForKey(eventId) - 1; t >= 0; --t) {
1248                         if (!printedApp) {
1249                             printedApp = true;
1250                             pw.println(appToString(userId, pkgName));
1251                             pw.increaseIndent();
1252                         }
1253                         printedEvents = true;
1254 
1255                         OngoingEvent ongoingEvent = ongoingEvents.valueAt(e, t);
1256 
1257                         pw.print(EconomicPolicy.eventToString(ongoingEvent.eventId));
1258                         if (ongoingEvent.tag != null) {
1259                             pw.print("(");
1260                             pw.print(ongoingEvent.tag);
1261                             pw.print(")");
1262                         }
1263                         pw.print(" runtime=");
1264                         TimeUtils.formatDuration(nowElapsed - ongoingEvent.startTimeElapsed, pw);
1265                         pw.print(" delta/sec=");
1266                         pw.print(cakeToString(ongoingEvent.getDeltaPerSec()));
1267                         final long ctp = ongoingEvent.getCtpPerSec();
1268                         if (ctp != 0) {
1269                             pw.print(" ctp/sec=");
1270                             pw.print(cakeToString(ongoingEvent.getCtpPerSec()));
1271                         }
1272                         pw.print(" refCount=");
1273                         pw.print(ongoingEvent.refCount);
1274                         pw.println();
1275                     }
1276                 }
1277 
1278                 if (printedApp) {
1279                     pw.decreaseIndent();
1280                 }
1281             }
1282         }
1283         if (!printedEvents) {
1284             pw.print("N/A");
1285         }
1286         pw.decreaseIndent();
1287     }
1288 }
1289