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