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.HOUR_IN_MILLIS;
21 
22 import static com.android.server.tare.TareUtils.appToString;
23 import static com.android.server.tare.TareUtils.cakeToString;
24 
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.os.Environment;
28 import android.os.SystemClock;
29 import android.os.UserHandle;
30 import android.util.ArraySet;
31 import android.util.AtomicFile;
32 import android.util.IndentingPrintWriter;
33 import android.util.Log;
34 import android.util.Pair;
35 import android.util.Slog;
36 import android.util.SparseArray;
37 import android.util.SparseArrayMap;
38 import android.util.SparseLongArray;
39 import android.util.Xml;
40 
41 import com.android.internal.annotations.GuardedBy;
42 import com.android.internal.annotations.VisibleForTesting;
43 import com.android.modules.utils.TypedXmlPullParser;
44 import com.android.modules.utils.TypedXmlSerializer;
45 
46 import org.xmlpull.v1.XmlPullParser;
47 import org.xmlpull.v1.XmlPullParserException;
48 
49 import java.io.File;
50 import java.io.FileInputStream;
51 import java.io.FileOutputStream;
52 import java.io.IOException;
53 import java.util.ArrayList;
54 import java.util.List;
55 
56 /**
57  * Maintains the current TARE state and handles writing it to disk and reading it back from disk.
58  */
59 public class Scribe {
60     private static final String TAG = "TARE-" + Scribe.class.getSimpleName();
61     private static final boolean DEBUG = InternalResourceService.DEBUG
62             || Log.isLoggable(TAG, Log.DEBUG);
63 
64     /** The maximum number of transactions to dump per ledger. */
65     private static final int MAX_NUM_TRANSACTION_DUMP = 25;
66     /**
67      * The maximum amount of time we'll keep a transaction around for.
68      */
69     private static final long MAX_TRANSACTION_AGE_MS = 8 * 24 * HOUR_IN_MILLIS;
70 
71     private static final String XML_TAG_HIGH_LEVEL_STATE = "irs-state";
72     private static final String XML_TAG_LEDGER = "ledger";
73     private static final String XML_TAG_TARE = "tare";
74     private static final String XML_TAG_TRANSACTION = "transaction";
75     private static final String XML_TAG_REWARD_BUCKET = "rewardBucket";
76     private static final String XML_TAG_USER = "user";
77     private static final String XML_TAG_PERIOD_REPORT = "report";
78 
79     private static final String XML_ATTR_CTP = "ctp";
80     private static final String XML_ATTR_DELTA = "delta";
81     private static final String XML_ATTR_EVENT_ID = "eventId";
82     private static final String XML_ATTR_TAG = "tag";
83     private static final String XML_ATTR_START_TIME = "startTime";
84     private static final String XML_ATTR_END_TIME = "endTime";
85     private static final String XML_ATTR_PACKAGE_NAME = "pkgName";
86     private static final String XML_ATTR_CURRENT_BALANCE = "currentBalance";
87     private static final String XML_ATTR_USER_ID = "userId";
88     private static final String XML_ATTR_VERSION = "version";
89     private static final String XML_ATTR_LAST_RECLAMATION_TIME = "lastReclamationTime";
90     private static final String XML_ATTR_LAST_STOCK_RECALCULATION_TIME =
91             "lastStockRecalculationTime";
92     private static final String XML_ATTR_REMAINING_CONSUMABLE_CAKES = "remainingConsumableCakes";
93     private static final String XML_ATTR_CONSUMPTION_LIMIT = "consumptionLimit";
94     private static final String XML_ATTR_TIME_SINCE_FIRST_SETUP_MS = "timeSinceFirstSetup";
95     private static final String XML_ATTR_PR_DISCHARGE = "discharge";
96     private static final String XML_ATTR_PR_BATTERY_LEVEL = "batteryLevel";
97     private static final String XML_ATTR_PR_PROFIT = "profit";
98     private static final String XML_ATTR_PR_NUM_PROFIT = "numProfits";
99     private static final String XML_ATTR_PR_LOSS = "loss";
100     private static final String XML_ATTR_PR_NUM_LOSS = "numLoss";
101     private static final String XML_ATTR_PR_REWARDS = "rewards";
102     private static final String XML_ATTR_PR_NUM_REWARDS = "numRewards";
103     private static final String XML_ATTR_PR_POS_REGULATIONS = "posRegulations";
104     private static final String XML_ATTR_PR_NUM_POS_REGULATIONS = "numPosRegulations";
105     private static final String XML_ATTR_PR_NEG_REGULATIONS = "negRegulations";
106     private static final String XML_ATTR_PR_NUM_NEG_REGULATIONS = "numNegRegulations";
107     private static final String XML_ATTR_PR_SCREEN_OFF_DURATION_MS = "screenOffDurationMs";
108     private static final String XML_ATTR_PR_SCREEN_OFF_DISCHARGE_MAH = "screenOffDischargeMah";
109 
110     /** Version of the file schema. */
111     private static final int STATE_FILE_VERSION = 0;
112     /** Minimum amount of time between consecutive writes. */
113     private static final long WRITE_DELAY = 30_000L;
114 
115     private final AtomicFile mStateFile;
116     private final InternalResourceService mIrs;
117     private final Analyst mAnalyst;
118 
119     /**
120      * The value of elapsed realtime since TARE was first setup that was read from disk.
121      * This will only be changed when the persisted file is read.
122      */
123     private long mLoadedTimeSinceFirstSetup;
124     @GuardedBy("mIrs.getLock()")
125     private long mLastReclamationTime;
126     @GuardedBy("mIrs.getLock()")
127     private long mLastStockRecalculationTime;
128     @GuardedBy("mIrs.getLock()")
129     private long mSatiatedConsumptionLimit;
130     @GuardedBy("mIrs.getLock()")
131     private long mRemainingConsumableCakes;
132     @GuardedBy("mIrs.getLock()")
133     private final SparseArrayMap<String, Ledger> mLedgers = new SparseArrayMap<>();
134     /** Offsets used to calculate the total realtime since each user was added. */
135     @GuardedBy("mIrs.getLock()")
136     private final SparseLongArray mRealtimeSinceUsersAddedOffsets = new SparseLongArray();
137 
138     private final Runnable mCleanRunnable = this::cleanupLedgers;
139     private final Runnable mWriteRunnable = this::writeState;
140 
Scribe(InternalResourceService irs, Analyst analyst)141     Scribe(InternalResourceService irs, Analyst analyst) {
142         this(irs, analyst, Environment.getDataSystemDirectory());
143     }
144 
145     @VisibleForTesting
Scribe(InternalResourceService irs, Analyst analyst, File dataDir)146     Scribe(InternalResourceService irs, Analyst analyst, File dataDir) {
147         mIrs = irs;
148         mAnalyst = analyst;
149 
150         final File tareDir = new File(dataDir, "tare");
151         //noinspection ResultOfMethodCallIgnored
152         tareDir.mkdirs();
153         mStateFile = new AtomicFile(new File(tareDir, "state.xml"), "tare");
154     }
155 
156     @GuardedBy("mIrs.getLock()")
adjustRemainingConsumableCakesLocked(long delta)157     void adjustRemainingConsumableCakesLocked(long delta) {
158         final long staleCakes = mRemainingConsumableCakes;
159         mRemainingConsumableCakes += delta;
160         if (mRemainingConsumableCakes < 0) {
161             Slog.w(TAG, "Overdrew consumable cakes by " + cakeToString(-mRemainingConsumableCakes));
162             // A negative value would interfere with allowing free actions, so set the minimum as 0.
163             mRemainingConsumableCakes = 0;
164         }
165         if (mRemainingConsumableCakes != staleCakes) {
166             // No point doing any work if there was no functional change.
167             postWrite();
168         }
169     }
170 
171     @GuardedBy("mIrs.getLock()")
discardLedgerLocked(final int userId, @NonNull final String pkgName)172     void discardLedgerLocked(final int userId, @NonNull final String pkgName) {
173         mLedgers.delete(userId, pkgName);
174         postWrite();
175     }
176 
177     @GuardedBy("mIrs.getLock()")
onUserRemovedLocked(final int userId)178     void onUserRemovedLocked(final int userId) {
179         mLedgers.delete(userId);
180         mRealtimeSinceUsersAddedOffsets.delete(userId);
181         postWrite();
182     }
183 
184     @GuardedBy("mIrs.getLock()")
getSatiatedConsumptionLimitLocked()185     long getSatiatedConsumptionLimitLocked() {
186         return mSatiatedConsumptionLimit;
187     }
188 
189     @GuardedBy("mIrs.getLock()")
getLastReclamationTimeLocked()190     long getLastReclamationTimeLocked() {
191         return mLastReclamationTime;
192     }
193 
194     @GuardedBy("mIrs.getLock()")
getLastStockRecalculationTimeLocked()195     long getLastStockRecalculationTimeLocked() {
196         return mLastStockRecalculationTime;
197     }
198 
199     @GuardedBy("mIrs.getLock()")
200     @NonNull
getLedgerLocked(final int userId, @NonNull final String pkgName)201     Ledger getLedgerLocked(final int userId, @NonNull final String pkgName) {
202         Ledger ledger = mLedgers.get(userId, pkgName);
203         if (ledger == null) {
204             ledger = new Ledger();
205             mLedgers.add(userId, pkgName, ledger);
206         }
207         return ledger;
208     }
209 
210     @GuardedBy("mIrs.getLock()")
211     @NonNull
getLedgersLocked()212     SparseArrayMap<String, Ledger> getLedgersLocked() {
213         return mLedgers;
214     }
215 
216     /**
217      * Returns the sum of credits granted to all apps on the system. This is expensive so don't
218      * call it for normal operation.
219      */
220     @GuardedBy("mIrs.getLock()")
getCakesInCirculationForLoggingLocked()221     long getCakesInCirculationForLoggingLocked() {
222         long sum = 0;
223         for (int uIdx = mLedgers.numMaps() - 1; uIdx >= 0; --uIdx) {
224             for (int pIdx = mLedgers.numElementsForKeyAt(uIdx) - 1; pIdx >= 0; --pIdx) {
225                 sum += mLedgers.valueAt(uIdx, pIdx).getCurrentBalance();
226             }
227         }
228         return sum;
229     }
230 
231     /** Returns the cumulative elapsed realtime since TARE was first setup. */
getRealtimeSinceFirstSetupMs(long nowElapsed)232     long getRealtimeSinceFirstSetupMs(long nowElapsed) {
233         return mLoadedTimeSinceFirstSetup + nowElapsed;
234     }
235 
236     /** Returns the total amount of cakes that remain to be consumed. */
237     @GuardedBy("mIrs.getLock()")
getRemainingConsumableCakesLocked()238     long getRemainingConsumableCakesLocked() {
239         return mRemainingConsumableCakes;
240     }
241 
242     @GuardedBy("mIrs.getLock()")
getRealtimeSinceUsersAddedLocked(long nowElapsed)243     SparseLongArray getRealtimeSinceUsersAddedLocked(long nowElapsed) {
244         final SparseLongArray realtimes = new SparseLongArray();
245         for (int i = mRealtimeSinceUsersAddedOffsets.size() - 1; i >= 0; --i) {
246             realtimes.put(mRealtimeSinceUsersAddedOffsets.keyAt(i),
247                     mRealtimeSinceUsersAddedOffsets.valueAt(i) + nowElapsed);
248         }
249         return realtimes;
250     }
251 
252     @GuardedBy("mIrs.getLock()")
loadFromDiskLocked()253     void loadFromDiskLocked() {
254         mLedgers.clear();
255         if (!recordExists()) {
256             mSatiatedConsumptionLimit = mIrs.getInitialSatiatedConsumptionLimitLocked();
257             mRemainingConsumableCakes = mIrs.getConsumptionLimitLocked();
258             return;
259         }
260         mSatiatedConsumptionLimit = 0;
261         mRemainingConsumableCakes = 0;
262 
263         final SparseArray<ArraySet<String>> installedPackagesPerUser = new SparseArray<>();
264         final SparseArrayMap<String, InstalledPackageInfo> installedPackages =
265                 mIrs.getInstalledPackages();
266         for (int uIdx = installedPackages.numMaps() - 1; uIdx >= 0; --uIdx) {
267             final int userId = installedPackages.keyAt(uIdx);
268 
269             for (int pIdx = installedPackages.numElementsForKeyAt(uIdx) - 1; pIdx >= 0; --pIdx) {
270                 final InstalledPackageInfo packageInfo = installedPackages.valueAt(uIdx, pIdx);
271                 if (packageInfo.uid != InstalledPackageInfo.NO_UID) {
272                     ArraySet<String> pkgsForUser = installedPackagesPerUser.get(userId);
273                     if (pkgsForUser == null) {
274                         pkgsForUser = new ArraySet<>();
275                         installedPackagesPerUser.put(userId, pkgsForUser);
276                     }
277                     pkgsForUser.add(packageInfo.packageName);
278                 }
279             }
280         }
281 
282         final List<Analyst.Report> reports = new ArrayList<>();
283         try (FileInputStream fis = mStateFile.openRead()) {
284             TypedXmlPullParser parser = Xml.resolvePullParser(fis);
285 
286             int eventType = parser.getEventType();
287             while (eventType != XmlPullParser.START_TAG
288                     && eventType != XmlPullParser.END_DOCUMENT) {
289                 eventType = parser.next();
290             }
291             if (eventType == XmlPullParser.END_DOCUMENT) {
292                 if (DEBUG) {
293                     Slog.w(TAG, "No persisted state.");
294                 }
295                 return;
296             }
297 
298             String tagName = parser.getName();
299             if (XML_TAG_TARE.equals(tagName)) {
300                 final int version = parser.getAttributeInt(null, XML_ATTR_VERSION);
301                 if (version < 0 || version > STATE_FILE_VERSION) {
302                     Slog.e(TAG, "Invalid version number (" + version + "), aborting file read");
303                     return;
304                 }
305             }
306 
307             final long now = System.currentTimeMillis();
308             final long endTimeCutoff = now - MAX_TRANSACTION_AGE_MS;
309             long earliestEndTime = Long.MAX_VALUE;
310             for (eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
311                     eventType = parser.next()) {
312                 if (eventType != XmlPullParser.START_TAG) {
313                     continue;
314                 }
315                 tagName = parser.getName();
316                 if (tagName == null) {
317                     continue;
318                 }
319 
320                 switch (tagName) {
321                     case XML_TAG_HIGH_LEVEL_STATE:
322                         mLastReclamationTime =
323                                 parser.getAttributeLong(null, XML_ATTR_LAST_RECLAMATION_TIME);
324                         mLastStockRecalculationTime = parser.getAttributeLong(null,
325                                 XML_ATTR_LAST_STOCK_RECALCULATION_TIME, 0);
326                         mLoadedTimeSinceFirstSetup =
327                                 parser.getAttributeLong(null, XML_ATTR_TIME_SINCE_FIRST_SETUP_MS,
328                                         // If there's no recorded time since first setup, then
329                                         // offset the current elapsed time so it doesn't shift the
330                                         // timing too much.
331                                         -SystemClock.elapsedRealtime());
332                         mSatiatedConsumptionLimit =
333                                 parser.getAttributeLong(null, XML_ATTR_CONSUMPTION_LIMIT,
334                                         mIrs.getInitialSatiatedConsumptionLimitLocked());
335                         final long consumptionLimit = mIrs.getConsumptionLimitLocked();
336                         mRemainingConsumableCakes = Math.min(consumptionLimit,
337                                 parser.getAttributeLong(null, XML_ATTR_REMAINING_CONSUMABLE_CAKES,
338                                         consumptionLimit));
339                         break;
340                     case XML_TAG_USER:
341                         earliestEndTime = Math.min(earliestEndTime,
342                                 readUserFromXmlLocked(
343                                         parser, installedPackagesPerUser, endTimeCutoff));
344                         break;
345                     case XML_TAG_PERIOD_REPORT:
346                         reports.add(readReportFromXml(parser));
347                         break;
348                     default:
349                         Slog.e(TAG, "Unexpected tag: " + tagName);
350                         break;
351                 }
352             }
353             mAnalyst.loadReports(reports);
354             scheduleCleanup(earliestEndTime);
355         } catch (IOException | XmlPullParserException e) {
356             Slog.wtf(TAG, "Error reading state from disk", e);
357         }
358     }
359 
360     @VisibleForTesting
postWrite()361     void postWrite() {
362         TareHandlerThread.getHandler().postDelayed(mWriteRunnable, WRITE_DELAY);
363     }
364 
recordExists()365     boolean recordExists() {
366         return mStateFile.exists();
367     }
368 
369     @GuardedBy("mIrs.getLock()")
setConsumptionLimitLocked(long limit)370     void setConsumptionLimitLocked(long limit) {
371         if (mRemainingConsumableCakes > limit) {
372             mRemainingConsumableCakes = limit;
373         } else if (limit > mSatiatedConsumptionLimit) {
374             final long diff = mSatiatedConsumptionLimit - mRemainingConsumableCakes;
375             mRemainingConsumableCakes = (limit - diff);
376         }
377         mSatiatedConsumptionLimit = limit;
378         postWrite();
379     }
380 
381     @GuardedBy("mIrs.getLock()")
setLastReclamationTimeLocked(long time)382     void setLastReclamationTimeLocked(long time) {
383         mLastReclamationTime = time;
384         postWrite();
385     }
386 
387     @GuardedBy("mIrs.getLock()")
setLastStockRecalculationTimeLocked(long time)388     void setLastStockRecalculationTimeLocked(long time) {
389         mLastStockRecalculationTime = time;
390         postWrite();
391     }
392 
393     @GuardedBy("mIrs.getLock()")
setUserAddedTimeLocked(int userId, long timeElapsed)394     void setUserAddedTimeLocked(int userId, long timeElapsed) {
395         // Use the current time as an offset so that when we persist the time, it correctly persists
396         // as "time since now".
397         mRealtimeSinceUsersAddedOffsets.put(userId, -timeElapsed);
398     }
399 
400     @GuardedBy("mIrs.getLock()")
tearDownLocked()401     void tearDownLocked() {
402         TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable);
403         TareHandlerThread.getHandler().removeCallbacks(mWriteRunnable);
404         mLedgers.clear();
405         mRemainingConsumableCakes = 0;
406         mSatiatedConsumptionLimit = 0;
407         mLastReclamationTime = 0;
408     }
409 
410     @VisibleForTesting
writeImmediatelyForTesting()411     void writeImmediatelyForTesting() {
412         mWriteRunnable.run();
413     }
414 
cleanupLedgers()415     private void cleanupLedgers() {
416         synchronized (mIrs.getLock()) {
417             TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable);
418             long earliestEndTime = Long.MAX_VALUE;
419             for (int uIdx = mLedgers.numMaps() - 1; uIdx >= 0; --uIdx) {
420                 final int userId = mLedgers.keyAt(uIdx);
421 
422                 for (int pIdx = mLedgers.numElementsForKey(userId) - 1; pIdx >= 0; --pIdx) {
423                     final String pkgName = mLedgers.keyAt(uIdx, pIdx);
424                     final Ledger ledger = mLedgers.get(userId, pkgName);
425                     final Ledger.Transaction transaction =
426                             ledger.removeOldTransactions(MAX_TRANSACTION_AGE_MS);
427                     if (transaction != null) {
428                         earliestEndTime = Math.min(earliestEndTime, transaction.endTimeMs);
429                     }
430                 }
431             }
432             scheduleCleanup(earliestEndTime);
433         }
434     }
435 
436     /**
437      * @param parser Xml parser at the beginning of a "<ledger/>" tag. The next "parser.next()" call
438      *               will take the parser into the body of the ledger tag.
439      * @return Newly instantiated ledger holding all the information we just read out of the xml
440      * tag, and the package name associated with the ledger.
441      */
442     @Nullable
readLedgerFromXml(TypedXmlPullParser parser, ArraySet<String> validPackages, long endTimeCutoff)443     private static Pair<String, Ledger> readLedgerFromXml(TypedXmlPullParser parser,
444             ArraySet<String> validPackages, long endTimeCutoff)
445             throws XmlPullParserException, IOException {
446         final String pkgName;
447         final long curBalance;
448         final List<Ledger.Transaction> transactions = new ArrayList<>();
449         final List<Ledger.RewardBucket> rewardBuckets = new ArrayList<>();
450 
451         pkgName = parser.getAttributeValue(null, XML_ATTR_PACKAGE_NAME);
452         curBalance = parser.getAttributeLong(null, XML_ATTR_CURRENT_BALANCE);
453 
454         final boolean isInstalled = validPackages.contains(pkgName);
455         if (!isInstalled) {
456             // Don't return early since we need to go through all the transaction tags and get
457             // to the end of the ledger tag.
458             Slog.w(TAG, "Invalid pkg " + pkgName + " is saved to disk");
459         }
460 
461         for (int eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
462                 eventType = parser.next()) {
463             final String tagName = parser.getName();
464             if (eventType == XmlPullParser.END_TAG) {
465                 if (XML_TAG_LEDGER.equals(tagName)) {
466                     // We've reached the end of the ledger tag.
467                     break;
468                 }
469                 continue;
470             }
471             if (eventType != XmlPullParser.START_TAG || tagName == null) {
472                 Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
473                 return null;
474             }
475             if (!isInstalled) {
476                 continue;
477             }
478             if (DEBUG) {
479                 Slog.d(TAG, "Starting ledger tag: " + tagName);
480             }
481             switch (tagName) {
482                 case XML_TAG_TRANSACTION:
483                     final long endTime = parser.getAttributeLong(null, XML_ATTR_END_TIME);
484                     if (endTime <= endTimeCutoff) {
485                         if (DEBUG) {
486                             Slog.d(TAG, "Skipping event because it's too old.");
487                         }
488                         continue;
489                     }
490                     final String tag = parser.getAttributeValue(null, XML_ATTR_TAG);
491                     final long startTime = parser.getAttributeLong(null, XML_ATTR_START_TIME);
492                     final int eventId = parser.getAttributeInt(null, XML_ATTR_EVENT_ID);
493                     final long delta = parser.getAttributeLong(null, XML_ATTR_DELTA);
494                     final long ctp = parser.getAttributeLong(null, XML_ATTR_CTP);
495                     transactions.add(
496                             new Ledger.Transaction(startTime, endTime, eventId, tag, delta, ctp));
497                     break;
498                 case XML_TAG_REWARD_BUCKET:
499                     rewardBuckets.add(readRewardBucketFromXml(parser));
500                     break;
501                 default:
502                     // Expecting only "transaction" and "rewardBucket" tags.
503                     Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
504                     return null;
505             }
506         }
507 
508         if (!isInstalled) {
509             return null;
510         }
511         return Pair.create(pkgName, new Ledger(curBalance, transactions, rewardBuckets));
512     }
513 
514     /**
515      * @param parser Xml parser at the beginning of a "<user>" tag. The next "parser.next()" call
516      *               will take the parser into the body of the user tag.
517      * @return The earliest valid transaction end time found for the user.
518      */
519     @GuardedBy("mIrs.getLock()")
readUserFromXmlLocked(TypedXmlPullParser parser, SparseArray<ArraySet<String>> installedPackagesPerUser, long endTimeCutoff)520     private long readUserFromXmlLocked(TypedXmlPullParser parser,
521             SparseArray<ArraySet<String>> installedPackagesPerUser,
522             long endTimeCutoff) throws XmlPullParserException, IOException {
523         int curUser = parser.getAttributeInt(null, XML_ATTR_USER_ID);
524         final ArraySet<String> installedPackages = installedPackagesPerUser.get(curUser);
525         if (installedPackages == null) {
526             Slog.w(TAG, "Invalid user " + curUser + " is saved to disk");
527             curUser = UserHandle.USER_NULL;
528             // Don't return early since we need to go through all the ledger tags and get to the end
529             // of the user tag.
530         }
531         if (curUser != UserHandle.USER_NULL) {
532             mRealtimeSinceUsersAddedOffsets.put(curUser,
533                             parser.getAttributeLong(null, XML_ATTR_TIME_SINCE_FIRST_SETUP_MS,
534                                     // If there's no recorded time since first setup, then
535                                     // offset the current elapsed time so it doesn't shift the
536                                     // timing too much.
537                                     -SystemClock.elapsedRealtime()));
538         }
539         long earliestEndTime = Long.MAX_VALUE;
540 
541         for (int eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
542                 eventType = parser.next()) {
543             final String tagName = parser.getName();
544             if (eventType == XmlPullParser.END_TAG) {
545                 if (XML_TAG_USER.equals(tagName)) {
546                     // We've reached the end of the user tag.
547                     break;
548                 }
549                 continue;
550             }
551             if (XML_TAG_LEDGER.equals(tagName)) {
552                 if (curUser == UserHandle.USER_NULL) {
553                     continue;
554                 }
555                 final Pair<String, Ledger> ledgerData =
556                         readLedgerFromXml(parser, installedPackages, endTimeCutoff);
557                 if (ledgerData == null) {
558                     continue;
559                 }
560                 final Ledger ledger = ledgerData.second;
561                 if (ledger != null) {
562                     mLedgers.add(curUser, ledgerData.first, ledger);
563                     final Ledger.Transaction transaction = ledger.getEarliestTransaction();
564                     if (transaction != null) {
565                         earliestEndTime = Math.min(earliestEndTime, transaction.endTimeMs);
566                     }
567                 }
568             } else {
569                 Slog.e(TAG, "Unknown tag: " + tagName);
570             }
571         }
572 
573         return earliestEndTime;
574     }
575 
576     /**
577      * @param parser Xml parser at the beginning of a {@link #XML_TAG_PERIOD_REPORT} tag. The next
578      *               "parser.next()" call will take the parser into the body of the report tag.
579      * @return Newly instantiated Report holding all the information we just read out of the xml tag
580      */
581     @NonNull
readReportFromXml(TypedXmlPullParser parser)582     private static Analyst.Report readReportFromXml(TypedXmlPullParser parser)
583             throws XmlPullParserException, IOException {
584         final Analyst.Report report = new Analyst.Report();
585 
586         report.cumulativeBatteryDischarge = parser.getAttributeInt(null, XML_ATTR_PR_DISCHARGE);
587         report.currentBatteryLevel = parser.getAttributeInt(null, XML_ATTR_PR_BATTERY_LEVEL);
588         report.cumulativeProfit = parser.getAttributeLong(null, XML_ATTR_PR_PROFIT);
589         report.numProfitableActions = parser.getAttributeInt(null, XML_ATTR_PR_NUM_PROFIT);
590         report.cumulativeLoss = parser.getAttributeLong(null, XML_ATTR_PR_LOSS);
591         report.numUnprofitableActions = parser.getAttributeInt(null, XML_ATTR_PR_NUM_LOSS);
592         report.cumulativeRewards = parser.getAttributeLong(null, XML_ATTR_PR_REWARDS);
593         report.numRewards = parser.getAttributeInt(null, XML_ATTR_PR_NUM_REWARDS);
594         report.cumulativePositiveRegulations =
595                 parser.getAttributeLong(null, XML_ATTR_PR_POS_REGULATIONS);
596         report.numPositiveRegulations =
597                 parser.getAttributeInt(null, XML_ATTR_PR_NUM_POS_REGULATIONS);
598         report.cumulativeNegativeRegulations =
599                 parser.getAttributeLong(null, XML_ATTR_PR_NEG_REGULATIONS);
600         report.numNegativeRegulations =
601                 parser.getAttributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS);
602         report.screenOffDurationMs =
603                 parser.getAttributeLong(null, XML_ATTR_PR_SCREEN_OFF_DURATION_MS, 0);
604         report.screenOffDischargeMah =
605                 parser.getAttributeLong(null, XML_ATTR_PR_SCREEN_OFF_DISCHARGE_MAH, 0);
606 
607         return report;
608     }
609 
610     /**
611      * @param parser Xml parser at the beginning of a {@value #XML_TAG_REWARD_BUCKET} tag. The next
612      *               "parser.next()" call will take the parser into the body of the tag.
613      * @return Newly instantiated {@link Ledger.RewardBucket} holding all the information we just
614      * read out of the xml tag.
615      */
616     @Nullable
readRewardBucketFromXml(TypedXmlPullParser parser)617     private static Ledger.RewardBucket readRewardBucketFromXml(TypedXmlPullParser parser)
618             throws XmlPullParserException, IOException {
619 
620         final Ledger.RewardBucket rewardBucket = new Ledger.RewardBucket();
621 
622         rewardBucket.startTimeMs = parser.getAttributeLong(null, XML_ATTR_START_TIME);
623 
624         for (int eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
625                 eventType = parser.next()) {
626             final String tagName = parser.getName();
627             if (eventType == XmlPullParser.END_TAG) {
628                 if (XML_TAG_REWARD_BUCKET.equals(tagName)) {
629                     // We've reached the end of the rewardBucket tag.
630                     break;
631                 }
632                 continue;
633             }
634             if (eventType != XmlPullParser.START_TAG || !XML_ATTR_DELTA.equals(tagName)) {
635                 // Expecting only delta tags.
636                 Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
637                 return null;
638             }
639 
640             final int eventId = parser.getAttributeInt(null, XML_ATTR_EVENT_ID);
641             final long delta = parser.getAttributeLong(null, XML_ATTR_DELTA);
642             rewardBucket.cumulativeDelta.put(eventId, delta);
643         }
644 
645         return rewardBucket;
646     }
647 
scheduleCleanup(long earliestEndTime)648     private void scheduleCleanup(long earliestEndTime) {
649         if (earliestEndTime == Long.MAX_VALUE) {
650             return;
651         }
652         // This is just cleanup to manage memory. We don't need to do it too often or at the exact
653         // intended real time, so the delay that comes from using the Handler (and is limited
654         // to uptime) should be fine.
655         final long delayMs = Math.max(HOUR_IN_MILLIS,
656                 earliestEndTime + MAX_TRANSACTION_AGE_MS - System.currentTimeMillis());
657         TareHandlerThread.getHandler().postDelayed(mCleanRunnable, delayMs);
658     }
659 
writeState()660     private void writeState() {
661         synchronized (mIrs.getLock()) {
662             TareHandlerThread.getHandler().removeCallbacks(mWriteRunnable);
663             // Remove mCleanRunnable callbacks since we're going to clean up the ledgers before
664             // writing anyway.
665             TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable);
666             if (mIrs.getEnabledMode() == ENABLED_MODE_OFF) {
667                 // If it's no longer enabled, we would have cleared all the data in memory and would
668                 // accidentally write an empty file, thus deleting all the history.
669                 return;
670             }
671             long earliestStoredEndTime = Long.MAX_VALUE;
672             try (FileOutputStream fos = mStateFile.startWrite()) {
673                 TypedXmlSerializer out = Xml.resolveSerializer(fos);
674                 out.startDocument(null, true);
675 
676                 out.startTag(null, XML_TAG_TARE);
677                 out.attributeInt(null, XML_ATTR_VERSION, STATE_FILE_VERSION);
678 
679                 out.startTag(null, XML_TAG_HIGH_LEVEL_STATE);
680                 out.attributeLong(null, XML_ATTR_LAST_RECLAMATION_TIME, mLastReclamationTime);
681                 out.attributeLong(null,
682                         XML_ATTR_LAST_STOCK_RECALCULATION_TIME, mLastStockRecalculationTime);
683                 out.attributeLong(null, XML_ATTR_TIME_SINCE_FIRST_SETUP_MS,
684                         mLoadedTimeSinceFirstSetup + SystemClock.elapsedRealtime());
685                 out.attributeLong(null, XML_ATTR_CONSUMPTION_LIMIT, mSatiatedConsumptionLimit);
686                 out.attributeLong(null, XML_ATTR_REMAINING_CONSUMABLE_CAKES,
687                         mRemainingConsumableCakes);
688                 out.endTag(null, XML_TAG_HIGH_LEVEL_STATE);
689 
690                 for (int uIdx = mLedgers.numMaps() - 1; uIdx >= 0; --uIdx) {
691                     final int userId = mLedgers.keyAt(uIdx);
692                     earliestStoredEndTime = Math.min(earliestStoredEndTime,
693                             writeUserLocked(out, userId));
694                 }
695 
696                 List<Analyst.Report> reports = mAnalyst.getReports();
697                 for (int i = 0, size = reports.size(); i < size; ++i) {
698                     writeReport(out, reports.get(i));
699                 }
700 
701                 out.endTag(null, XML_TAG_TARE);
702 
703                 out.endDocument();
704                 mStateFile.finishWrite(fos);
705             } catch (IOException e) {
706                 Slog.e(TAG, "Error writing state to disk", e);
707             }
708             scheduleCleanup(earliestStoredEndTime);
709         }
710     }
711 
712     @GuardedBy("mIrs.getLock()")
writeUserLocked(@onNull TypedXmlSerializer out, final int userId)713     private long writeUserLocked(@NonNull TypedXmlSerializer out, final int userId)
714             throws IOException {
715         final int uIdx = mLedgers.indexOfKey(userId);
716         long earliestStoredEndTime = Long.MAX_VALUE;
717 
718         out.startTag(null, XML_TAG_USER);
719         out.attributeInt(null, XML_ATTR_USER_ID, userId);
720         out.attributeLong(null, XML_ATTR_TIME_SINCE_FIRST_SETUP_MS,
721                 mRealtimeSinceUsersAddedOffsets.get(userId, mLoadedTimeSinceFirstSetup)
722                         + SystemClock.elapsedRealtime());
723         for (int pIdx = mLedgers.numElementsForKey(userId) - 1; pIdx >= 0; --pIdx) {
724             final String pkgName = mLedgers.keyAt(uIdx, pIdx);
725             final Ledger ledger = mLedgers.get(userId, pkgName);
726             // Remove old transactions so we don't waste space storing them.
727             ledger.removeOldTransactions(MAX_TRANSACTION_AGE_MS);
728 
729             out.startTag(null, XML_TAG_LEDGER);
730             out.attribute(null, XML_ATTR_PACKAGE_NAME, pkgName);
731             out.attributeLong(null,
732                     XML_ATTR_CURRENT_BALANCE, ledger.getCurrentBalance());
733 
734             final List<Ledger.Transaction> transactions = ledger.getTransactions();
735             for (int t = 0; t < transactions.size(); ++t) {
736                 Ledger.Transaction transaction = transactions.get(t);
737                 if (t == 0) {
738                     earliestStoredEndTime = Math.min(earliestStoredEndTime, transaction.endTimeMs);
739                 }
740                 writeTransaction(out, transaction);
741             }
742 
743             final List<Ledger.RewardBucket> rewardBuckets = ledger.getRewardBuckets();
744             for (int r = 0; r < rewardBuckets.size(); ++r) {
745                 writeRewardBucket(out, rewardBuckets.get(r));
746             }
747             out.endTag(null, XML_TAG_LEDGER);
748         }
749         out.endTag(null, XML_TAG_USER);
750 
751         return earliestStoredEndTime;
752     }
753 
writeTransaction(@onNull TypedXmlSerializer out, @NonNull Ledger.Transaction transaction)754     private static void writeTransaction(@NonNull TypedXmlSerializer out,
755             @NonNull Ledger.Transaction transaction) throws IOException {
756         out.startTag(null, XML_TAG_TRANSACTION);
757         out.attributeLong(null, XML_ATTR_START_TIME, transaction.startTimeMs);
758         out.attributeLong(null, XML_ATTR_END_TIME, transaction.endTimeMs);
759         out.attributeInt(null, XML_ATTR_EVENT_ID, transaction.eventId);
760         if (transaction.tag != null) {
761             out.attribute(null, XML_ATTR_TAG, transaction.tag);
762         }
763         out.attributeLong(null, XML_ATTR_DELTA, transaction.delta);
764         out.attributeLong(null, XML_ATTR_CTP, transaction.ctp);
765         out.endTag(null, XML_TAG_TRANSACTION);
766     }
767 
writeRewardBucket(@onNull TypedXmlSerializer out, @NonNull Ledger.RewardBucket rewardBucket)768     private static void writeRewardBucket(@NonNull TypedXmlSerializer out,
769             @NonNull Ledger.RewardBucket rewardBucket) throws IOException {
770         final int numEvents = rewardBucket.cumulativeDelta.size();
771         if (numEvents == 0) {
772             return;
773         }
774         out.startTag(null, XML_TAG_REWARD_BUCKET);
775         out.attributeLong(null, XML_ATTR_START_TIME, rewardBucket.startTimeMs);
776         for (int i = 0; i < numEvents; ++i) {
777             out.startTag(null, XML_ATTR_DELTA);
778             out.attributeInt(null, XML_ATTR_EVENT_ID, rewardBucket.cumulativeDelta.keyAt(i));
779             out.attributeLong(null, XML_ATTR_DELTA, rewardBucket.cumulativeDelta.valueAt(i));
780             out.endTag(null, XML_ATTR_DELTA);
781         }
782         out.endTag(null, XML_TAG_REWARD_BUCKET);
783     }
784 
writeReport(@onNull TypedXmlSerializer out, @NonNull Analyst.Report report)785     private static void writeReport(@NonNull TypedXmlSerializer out,
786             @NonNull Analyst.Report report) throws IOException {
787         out.startTag(null, XML_TAG_PERIOD_REPORT);
788         out.attributeInt(null, XML_ATTR_PR_DISCHARGE, report.cumulativeBatteryDischarge);
789         out.attributeInt(null, XML_ATTR_PR_BATTERY_LEVEL, report.currentBatteryLevel);
790         out.attributeLong(null, XML_ATTR_PR_PROFIT, report.cumulativeProfit);
791         out.attributeInt(null, XML_ATTR_PR_NUM_PROFIT, report.numProfitableActions);
792         out.attributeLong(null, XML_ATTR_PR_LOSS, report.cumulativeLoss);
793         out.attributeInt(null, XML_ATTR_PR_NUM_LOSS, report.numUnprofitableActions);
794         out.attributeLong(null, XML_ATTR_PR_REWARDS, report.cumulativeRewards);
795         out.attributeInt(null, XML_ATTR_PR_NUM_REWARDS, report.numRewards);
796         out.attributeLong(null, XML_ATTR_PR_POS_REGULATIONS, report.cumulativePositiveRegulations);
797         out.attributeInt(null, XML_ATTR_PR_NUM_POS_REGULATIONS, report.numPositiveRegulations);
798         out.attributeLong(null, XML_ATTR_PR_NEG_REGULATIONS, report.cumulativeNegativeRegulations);
799         out.attributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS, report.numNegativeRegulations);
800         out.attributeLong(null, XML_ATTR_PR_SCREEN_OFF_DURATION_MS, report.screenOffDurationMs);
801         out.attributeLong(null, XML_ATTR_PR_SCREEN_OFF_DISCHARGE_MAH, report.screenOffDischargeMah);
802         out.endTag(null, XML_TAG_PERIOD_REPORT);
803     }
804 
805     @GuardedBy("mIrs.getLock()")
dumpLocked(IndentingPrintWriter pw, boolean dumpAll)806     void dumpLocked(IndentingPrintWriter pw, boolean dumpAll) {
807         pw.println("Ledgers:");
808         pw.increaseIndent();
809         mLedgers.forEach((userId, pkgName, ledger) -> {
810             pw.print(appToString(userId, pkgName));
811             if (mIrs.isSystem(userId, pkgName)) {
812                 pw.print(" (system)");
813             }
814             pw.println();
815             pw.increaseIndent();
816             ledger.dump(pw, dumpAll ? Integer.MAX_VALUE : MAX_NUM_TRANSACTION_DUMP);
817             pw.decreaseIndent();
818         });
819         pw.decreaseIndent();
820     }
821 }
822