• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.tare;
18 
19 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
20 
21 import static com.android.server.tare.TareUtils.appToString;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.content.pm.PackageInfo;
26 import android.os.Environment;
27 import android.os.UserHandle;
28 import android.util.ArraySet;
29 import android.util.AtomicFile;
30 import android.util.IndentingPrintWriter;
31 import android.util.Log;
32 import android.util.Pair;
33 import android.util.Slog;
34 import android.util.SparseArray;
35 import android.util.SparseArrayMap;
36 import android.util.TypedXmlPullParser;
37 import android.util.TypedXmlSerializer;
38 import android.util.Xml;
39 
40 import com.android.internal.annotations.GuardedBy;
41 import com.android.internal.annotations.VisibleForTesting;
42 
43 import org.xmlpull.v1.XmlPullParser;
44 import org.xmlpull.v1.XmlPullParserException;
45 
46 import java.io.File;
47 import java.io.FileInputStream;
48 import java.io.FileOutputStream;
49 import java.io.IOException;
50 import java.util.ArrayList;
51 import java.util.List;
52 
53 /**
54  * Maintains the current TARE state and handles writing it to disk and reading it back from disk.
55  */
56 public class Scribe {
57     private static final String TAG = "TARE-" + Scribe.class.getSimpleName();
58     private static final boolean DEBUG = InternalResourceService.DEBUG
59             || Log.isLoggable(TAG, Log.DEBUG);
60 
61     /** The maximum number of transactions to dump per ledger. */
62     private static final int MAX_NUM_TRANSACTION_DUMP = 25;
63     /**
64      * The maximum amount of time we'll keep a transaction around for.
65      * For now, only keep transactions we actually have a use for. We can increase it if we want
66      * to use older transactions or provide older transactions to apps.
67      */
68     private static final long MAX_TRANSACTION_AGE_MS = 24 * HOUR_IN_MILLIS;
69 
70     private static final String XML_TAG_HIGH_LEVEL_STATE = "irs-state";
71     private static final String XML_TAG_LEDGER = "ledger";
72     private static final String XML_TAG_TARE = "tare";
73     private static final String XML_TAG_TRANSACTION = "transaction";
74     private static final String XML_TAG_USER = "user";
75     private static final String XML_TAG_PERIOD_REPORT = "report";
76 
77     private static final String XML_ATTR_CTP = "ctp";
78     private static final String XML_ATTR_DELTA = "delta";
79     private static final String XML_ATTR_EVENT_ID = "eventId";
80     private static final String XML_ATTR_TAG = "tag";
81     private static final String XML_ATTR_START_TIME = "startTime";
82     private static final String XML_ATTR_END_TIME = "endTime";
83     private static final String XML_ATTR_PACKAGE_NAME = "pkgName";
84     private static final String XML_ATTR_CURRENT_BALANCE = "currentBalance";
85     private static final String XML_ATTR_USER_ID = "userId";
86     private static final String XML_ATTR_VERSION = "version";
87     private static final String XML_ATTR_LAST_RECLAMATION_TIME = "lastReclamationTime";
88     private static final String XML_ATTR_REMAINING_CONSUMABLE_CAKES = "remainingConsumableCakes";
89     private static final String XML_ATTR_CONSUMPTION_LIMIT = "consumptionLimit";
90     private static final String XML_ATTR_PR_DISCHARGE = "discharge";
91     private static final String XML_ATTR_PR_BATTERY_LEVEL = "batteryLevel";
92     private static final String XML_ATTR_PR_PROFIT = "profit";
93     private static final String XML_ATTR_PR_NUM_PROFIT = "numProfits";
94     private static final String XML_ATTR_PR_LOSS = "loss";
95     private static final String XML_ATTR_PR_NUM_LOSS = "numLoss";
96     private static final String XML_ATTR_PR_REWARDS = "rewards";
97     private static final String XML_ATTR_PR_NUM_REWARDS = "numRewards";
98     private static final String XML_ATTR_PR_POS_REGULATIONS = "posRegulations";
99     private static final String XML_ATTR_PR_NUM_POS_REGULATIONS = "numPosRegulations";
100     private static final String XML_ATTR_PR_NEG_REGULATIONS = "negRegulations";
101     private static final String XML_ATTR_PR_NUM_NEG_REGULATIONS = "numNegRegulations";
102 
103     /** Version of the file schema. */
104     private static final int STATE_FILE_VERSION = 0;
105     /** Minimum amount of time between consecutive writes. */
106     private static final long WRITE_DELAY = 30_000L;
107 
108     private final AtomicFile mStateFile;
109     private final InternalResourceService mIrs;
110     private final Analyst mAnalyst;
111 
112     @GuardedBy("mIrs.getLock()")
113     private long mLastReclamationTime;
114     @GuardedBy("mIrs.getLock()")
115     private long mSatiatedConsumptionLimit;
116     @GuardedBy("mIrs.getLock()")
117     private long mRemainingConsumableCakes;
118     @GuardedBy("mIrs.getLock()")
119     private final SparseArrayMap<String, Ledger> mLedgers = new SparseArrayMap<>();
120 
121     private final Runnable mCleanRunnable = this::cleanupLedgers;
122     private final Runnable mWriteRunnable = this::writeState;
123 
Scribe(InternalResourceService irs, Analyst analyst)124     Scribe(InternalResourceService irs, Analyst analyst) {
125         this(irs, analyst, Environment.getDataSystemDirectory());
126     }
127 
128     @VisibleForTesting
Scribe(InternalResourceService irs, Analyst analyst, File dataDir)129     Scribe(InternalResourceService irs, Analyst analyst, File dataDir) {
130         mIrs = irs;
131         mAnalyst = analyst;
132 
133         final File tareDir = new File(dataDir, "tare");
134         //noinspection ResultOfMethodCallIgnored
135         tareDir.mkdirs();
136         mStateFile = new AtomicFile(new File(tareDir, "state.xml"), "tare");
137     }
138 
139     @GuardedBy("mIrs.getLock()")
adjustRemainingConsumableCakesLocked(long delta)140     void adjustRemainingConsumableCakesLocked(long delta) {
141         if (delta != 0) {
142             // No point doing any work if the change is 0.
143             mRemainingConsumableCakes += delta;
144             postWrite();
145         }
146     }
147 
148     @GuardedBy("mIrs.getLock()")
discardLedgerLocked(final int userId, @NonNull final String pkgName)149     void discardLedgerLocked(final int userId, @NonNull final String pkgName) {
150         mLedgers.delete(userId, pkgName);
151         postWrite();
152     }
153 
154     @GuardedBy("mIrs.getLock()")
getSatiatedConsumptionLimitLocked()155     long getSatiatedConsumptionLimitLocked() {
156         return mSatiatedConsumptionLimit;
157     }
158 
159     @GuardedBy("mIrs.getLock()")
getLastReclamationTimeLocked()160     long getLastReclamationTimeLocked() {
161         return mLastReclamationTime;
162     }
163 
164     @GuardedBy("mIrs.getLock()")
165     @NonNull
getLedgerLocked(final int userId, @NonNull final String pkgName)166     Ledger getLedgerLocked(final int userId, @NonNull final String pkgName) {
167         Ledger ledger = mLedgers.get(userId, pkgName);
168         if (ledger == null) {
169             ledger = new Ledger();
170             mLedgers.add(userId, pkgName, ledger);
171         }
172         return ledger;
173     }
174 
175     @GuardedBy("mIrs.getLock()")
176     @NonNull
getLedgersLocked()177     SparseArrayMap<String, Ledger> getLedgersLocked() {
178         return mLedgers;
179     }
180 
181     /**
182      * Returns the sum of credits granted to all apps on the system. This is expensive so don't
183      * call it for normal operation.
184      */
185     @GuardedBy("mIrs.getLock()")
getCakesInCirculationForLoggingLocked()186     long getCakesInCirculationForLoggingLocked() {
187         long sum = 0;
188         for (int uIdx = mLedgers.numMaps() - 1; uIdx >= 0; --uIdx) {
189             for (int pIdx = mLedgers.numElementsForKeyAt(uIdx) - 1; pIdx >= 0; --pIdx) {
190                 sum += mLedgers.valueAt(uIdx, pIdx).getCurrentBalance();
191             }
192         }
193         return sum;
194     }
195 
196     /** Returns the total amount of cakes that remain to be consumed. */
197     @GuardedBy("mIrs.getLock()")
getRemainingConsumableCakesLocked()198     long getRemainingConsumableCakesLocked() {
199         return mRemainingConsumableCakes;
200     }
201 
202     @GuardedBy("mIrs.getLock()")
loadFromDiskLocked()203     void loadFromDiskLocked() {
204         mLedgers.clear();
205         if (!recordExists()) {
206             mSatiatedConsumptionLimit = mIrs.getInitialSatiatedConsumptionLimitLocked();
207             mRemainingConsumableCakes = mIrs.getConsumptionLimitLocked();
208             return;
209         }
210         mSatiatedConsumptionLimit = 0;
211         mRemainingConsumableCakes = 0;
212 
213         final SparseArray<ArraySet<String>> installedPackagesPerUser = new SparseArray<>();
214         final List<PackageInfo> installedPackages = mIrs.getInstalledPackages();
215         for (int i = 0; i < installedPackages.size(); ++i) {
216             final PackageInfo packageInfo = installedPackages.get(i);
217             if (packageInfo.applicationInfo != null) {
218                 final int userId = UserHandle.getUserId(packageInfo.applicationInfo.uid);
219                 ArraySet<String> pkgsForUser = installedPackagesPerUser.get(userId);
220                 if (pkgsForUser == null) {
221                     pkgsForUser = new ArraySet<>();
222                     installedPackagesPerUser.put(userId, pkgsForUser);
223                 }
224                 pkgsForUser.add(packageInfo.packageName);
225             }
226         }
227 
228         final List<Analyst.Report> reports = new ArrayList<>();
229         try (FileInputStream fis = mStateFile.openRead()) {
230             TypedXmlPullParser parser = Xml.resolvePullParser(fis);
231 
232             int eventType = parser.getEventType();
233             while (eventType != XmlPullParser.START_TAG
234                     && eventType != XmlPullParser.END_DOCUMENT) {
235                 eventType = parser.next();
236             }
237             if (eventType == XmlPullParser.END_DOCUMENT) {
238                 if (DEBUG) {
239                     Slog.w(TAG, "No persisted state.");
240                 }
241                 return;
242             }
243 
244             String tagName = parser.getName();
245             if (XML_TAG_TARE.equals(tagName)) {
246                 final int version = parser.getAttributeInt(null, XML_ATTR_VERSION);
247                 if (version < 0 || version > STATE_FILE_VERSION) {
248                     Slog.e(TAG, "Invalid version number (" + version + "), aborting file read");
249                     return;
250                 }
251             }
252 
253             final long endTimeCutoff = System.currentTimeMillis() - MAX_TRANSACTION_AGE_MS;
254             long earliestEndTime = Long.MAX_VALUE;
255             for (eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
256                     eventType = parser.next()) {
257                 if (eventType != XmlPullParser.START_TAG) {
258                     continue;
259                 }
260                 tagName = parser.getName();
261                 if (tagName == null) {
262                     continue;
263                 }
264 
265                 switch (tagName) {
266                     case XML_TAG_HIGH_LEVEL_STATE:
267                         mLastReclamationTime =
268                                 parser.getAttributeLong(null, XML_ATTR_LAST_RECLAMATION_TIME);
269                         mSatiatedConsumptionLimit =
270                                 parser.getAttributeLong(null, XML_ATTR_CONSUMPTION_LIMIT,
271                                         mIrs.getInitialSatiatedConsumptionLimitLocked());
272                         final long consumptionLimit = mIrs.getConsumptionLimitLocked();
273                         mRemainingConsumableCakes = Math.min(consumptionLimit,
274                                 parser.getAttributeLong(null, XML_ATTR_REMAINING_CONSUMABLE_CAKES,
275                                         consumptionLimit));
276                         break;
277                     case XML_TAG_USER:
278                         earliestEndTime = Math.min(earliestEndTime,
279                                 readUserFromXmlLocked(
280                                         parser, installedPackagesPerUser, endTimeCutoff));
281                         break;
282                     case XML_TAG_PERIOD_REPORT:
283                         reports.add(readReportFromXml(parser));
284                         break;
285                     default:
286                         Slog.e(TAG, "Unexpected tag: " + tagName);
287                         break;
288                 }
289             }
290             mAnalyst.loadReports(reports);
291             scheduleCleanup(earliestEndTime);
292         } catch (IOException | XmlPullParserException e) {
293             Slog.wtf(TAG, "Error reading state from disk", e);
294         }
295     }
296 
297     @VisibleForTesting
postWrite()298     void postWrite() {
299         TareHandlerThread.getHandler().postDelayed(mWriteRunnable, WRITE_DELAY);
300     }
301 
recordExists()302     boolean recordExists() {
303         return mStateFile.exists();
304     }
305 
306     @GuardedBy("mIrs.getLock()")
setConsumptionLimitLocked(long limit)307     void setConsumptionLimitLocked(long limit) {
308         if (mRemainingConsumableCakes > limit) {
309             mRemainingConsumableCakes = limit;
310         } else if (limit > mSatiatedConsumptionLimit) {
311             final long diff = mSatiatedConsumptionLimit - mRemainingConsumableCakes;
312             mRemainingConsumableCakes = (limit - diff);
313         }
314         mSatiatedConsumptionLimit = limit;
315         postWrite();
316     }
317 
318     @GuardedBy("mIrs.getLock()")
setLastReclamationTimeLocked(long time)319     void setLastReclamationTimeLocked(long time) {
320         mLastReclamationTime = time;
321         postWrite();
322     }
323 
324     @GuardedBy("mIrs.getLock()")
tearDownLocked()325     void tearDownLocked() {
326         TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable);
327         TareHandlerThread.getHandler().removeCallbacks(mWriteRunnable);
328         mLedgers.clear();
329         mRemainingConsumableCakes = 0;
330         mSatiatedConsumptionLimit = 0;
331         mLastReclamationTime = 0;
332     }
333 
334     @VisibleForTesting
writeImmediatelyForTesting()335     void writeImmediatelyForTesting() {
336         mWriteRunnable.run();
337     }
338 
cleanupLedgers()339     private void cleanupLedgers() {
340         synchronized (mIrs.getLock()) {
341             TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable);
342             long earliestEndTime = Long.MAX_VALUE;
343             for (int uIdx = mLedgers.numMaps() - 1; uIdx >= 0; --uIdx) {
344                 final int userId = mLedgers.keyAt(uIdx);
345 
346                 for (int pIdx = mLedgers.numElementsForKey(userId) - 1; pIdx >= 0; --pIdx) {
347                     final String pkgName = mLedgers.keyAt(uIdx, pIdx);
348                     final Ledger ledger = mLedgers.get(userId, pkgName);
349                     ledger.removeOldTransactions(MAX_TRANSACTION_AGE_MS);
350                     Ledger.Transaction transaction = ledger.getEarliestTransaction();
351                     if (transaction != null) {
352                         earliestEndTime = Math.min(earliestEndTime, transaction.endTimeMs);
353                     }
354                 }
355             }
356             scheduleCleanup(earliestEndTime);
357         }
358     }
359 
360     /**
361      * @param parser Xml parser at the beginning of a "<ledger/>" tag. The next "parser.next()" call
362      *               will take the parser into the body of the ledger tag.
363      * @return Newly instantiated ledger holding all the information we just read out of the xml
364      * tag, and the package name associated with the ledger.
365      */
366     @Nullable
readLedgerFromXml(TypedXmlPullParser parser, ArraySet<String> validPackages, long endTimeCutoff)367     private static Pair<String, Ledger> readLedgerFromXml(TypedXmlPullParser parser,
368             ArraySet<String> validPackages, long endTimeCutoff)
369             throws XmlPullParserException, IOException {
370         final String pkgName;
371         final long curBalance;
372         final List<Ledger.Transaction> transactions = new ArrayList<>();
373 
374         pkgName = parser.getAttributeValue(null, XML_ATTR_PACKAGE_NAME);
375         curBalance = parser.getAttributeLong(null, XML_ATTR_CURRENT_BALANCE);
376 
377         final boolean isInstalled = validPackages.contains(pkgName);
378         if (!isInstalled) {
379             // Don't return early since we need to go through all the transaction tags and get
380             // to the end of the ledger tag.
381             Slog.w(TAG, "Invalid pkg " + pkgName + " is saved to disk");
382         }
383 
384         for (int eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
385                 eventType = parser.next()) {
386             final String tagName = parser.getName();
387             if (eventType == XmlPullParser.END_TAG) {
388                 if (XML_TAG_LEDGER.equals(tagName)) {
389                     // We've reached the end of the ledger tag.
390                     break;
391                 }
392                 continue;
393             }
394             if (eventType != XmlPullParser.START_TAG || !XML_TAG_TRANSACTION.equals(tagName)) {
395                 // Expecting only "transaction" tags.
396                 Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
397                 return null;
398             }
399             if (!isInstalled) {
400                 continue;
401             }
402             if (DEBUG) {
403                 Slog.d(TAG, "Starting ledger tag: " + tagName);
404             }
405             final String tag = parser.getAttributeValue(null, XML_ATTR_TAG);
406             final long startTime = parser.getAttributeLong(null, XML_ATTR_START_TIME);
407             final long endTime = parser.getAttributeLong(null, XML_ATTR_END_TIME);
408             final int eventId = parser.getAttributeInt(null, XML_ATTR_EVENT_ID);
409             final long delta = parser.getAttributeLong(null, XML_ATTR_DELTA);
410             final long ctp = parser.getAttributeLong(null, XML_ATTR_CTP);
411             if (endTime <= endTimeCutoff) {
412                 if (DEBUG) {
413                     Slog.d(TAG, "Skipping event because it's too old.");
414                 }
415                 continue;
416             }
417             transactions.add(new Ledger.Transaction(startTime, endTime, eventId, tag, delta, ctp));
418         }
419 
420         if (!isInstalled) {
421             return null;
422         }
423         return Pair.create(pkgName, new Ledger(curBalance, transactions));
424     }
425 
426     /**
427      * @param parser Xml parser at the beginning of a "<user>" tag. The next "parser.next()" call
428      *               will take the parser into the body of the user tag.
429      * @return The earliest valid transaction end time found for the user.
430      */
431     @GuardedBy("mIrs.getLock()")
readUserFromXmlLocked(TypedXmlPullParser parser, SparseArray<ArraySet<String>> installedPackagesPerUser, long endTimeCutoff)432     private long readUserFromXmlLocked(TypedXmlPullParser parser,
433             SparseArray<ArraySet<String>> installedPackagesPerUser,
434             long endTimeCutoff) throws XmlPullParserException, IOException {
435         int curUser = parser.getAttributeInt(null, XML_ATTR_USER_ID);
436         final ArraySet<String> installedPackages = installedPackagesPerUser.get(curUser);
437         if (installedPackages == null) {
438             Slog.w(TAG, "Invalid user " + curUser + " is saved to disk");
439             curUser = UserHandle.USER_NULL;
440             // Don't return early since we need to go through all the ledger tags and get to the end
441             // of the user tag.
442         }
443         long earliestEndTime = Long.MAX_VALUE;
444 
445         for (int eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
446                 eventType = parser.next()) {
447             final String tagName = parser.getName();
448             if (eventType == XmlPullParser.END_TAG) {
449                 if (XML_TAG_USER.equals(tagName)) {
450                     // We've reached the end of the user tag.
451                     break;
452                 }
453                 continue;
454             }
455             if (XML_TAG_LEDGER.equals(tagName)) {
456                 if (curUser == UserHandle.USER_NULL) {
457                     continue;
458                 }
459                 final Pair<String, Ledger> ledgerData =
460                         readLedgerFromXml(parser, installedPackages, endTimeCutoff);
461                 if (ledgerData == null) {
462                     continue;
463                 }
464                 final Ledger ledger = ledgerData.second;
465                 if (ledger != null) {
466                     mLedgers.add(curUser, ledgerData.first, ledger);
467                     final Ledger.Transaction transaction = ledger.getEarliestTransaction();
468                     if (transaction != null) {
469                         earliestEndTime = Math.min(earliestEndTime, transaction.endTimeMs);
470                     }
471                 }
472             } else {
473                 Slog.e(TAG, "Unknown tag: " + tagName);
474             }
475         }
476 
477         return earliestEndTime;
478     }
479 
480 
481     /**
482      * @param parser Xml parser at the beginning of a {@link #XML_TAG_PERIOD_REPORT} tag. The next
483      *               "parser.next()" call will take the parser into the body of the report tag.
484      * @return Newly instantiated Report holding all the information we just read out of the xml tag
485      */
486     @NonNull
readReportFromXml(TypedXmlPullParser parser)487     private static Analyst.Report readReportFromXml(TypedXmlPullParser parser)
488             throws XmlPullParserException, IOException {
489         final Analyst.Report report = new Analyst.Report();
490 
491         report.cumulativeBatteryDischarge = parser.getAttributeInt(null, XML_ATTR_PR_DISCHARGE);
492         report.currentBatteryLevel = parser.getAttributeInt(null, XML_ATTR_PR_BATTERY_LEVEL);
493         report.cumulativeProfit = parser.getAttributeLong(null, XML_ATTR_PR_PROFIT);
494         report.numProfitableActions = parser.getAttributeInt(null, XML_ATTR_PR_NUM_PROFIT);
495         report.cumulativeLoss = parser.getAttributeLong(null, XML_ATTR_PR_LOSS);
496         report.numUnprofitableActions = parser.getAttributeInt(null, XML_ATTR_PR_NUM_LOSS);
497         report.cumulativeRewards = parser.getAttributeLong(null, XML_ATTR_PR_REWARDS);
498         report.numRewards = parser.getAttributeInt(null, XML_ATTR_PR_NUM_REWARDS);
499         report.cumulativePositiveRegulations =
500                 parser.getAttributeLong(null, XML_ATTR_PR_POS_REGULATIONS);
501         report.numPositiveRegulations =
502                 parser.getAttributeInt(null, XML_ATTR_PR_NUM_POS_REGULATIONS);
503         report.cumulativeNegativeRegulations =
504                 parser.getAttributeLong(null, XML_ATTR_PR_NEG_REGULATIONS);
505         report.numNegativeRegulations =
506                 parser.getAttributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS);
507 
508         return report;
509     }
510 
scheduleCleanup(long earliestEndTime)511     private void scheduleCleanup(long earliestEndTime) {
512         if (earliestEndTime == Long.MAX_VALUE) {
513             return;
514         }
515         // This is just cleanup to manage memory. We don't need to do it too often or at the exact
516         // intended real time, so the delay that comes from using the Handler (and is limited
517         // to uptime) should be fine.
518         final long delayMs = Math.max(HOUR_IN_MILLIS,
519                 earliestEndTime + MAX_TRANSACTION_AGE_MS - System.currentTimeMillis());
520         TareHandlerThread.getHandler().postDelayed(mCleanRunnable, delayMs);
521     }
522 
writeState()523     private void writeState() {
524         synchronized (mIrs.getLock()) {
525             TareHandlerThread.getHandler().removeCallbacks(mWriteRunnable);
526             // Remove mCleanRunnable callbacks since we're going to clean up the ledgers before
527             // writing anyway.
528             TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable);
529             if (!mIrs.isEnabled()) {
530                 // If it's no longer enabled, we would have cleared all the data in memory and would
531                 // accidentally write an empty file, thus deleting all the history.
532                 return;
533             }
534             long earliestStoredEndTime = Long.MAX_VALUE;
535             try (FileOutputStream fos = mStateFile.startWrite()) {
536                 TypedXmlSerializer out = Xml.resolveSerializer(fos);
537                 out.startDocument(null, true);
538 
539                 out.startTag(null, XML_TAG_TARE);
540                 out.attributeInt(null, XML_ATTR_VERSION, STATE_FILE_VERSION);
541 
542                 out.startTag(null, XML_TAG_HIGH_LEVEL_STATE);
543                 out.attributeLong(null, XML_ATTR_LAST_RECLAMATION_TIME, mLastReclamationTime);
544                 out.attributeLong(null, XML_ATTR_CONSUMPTION_LIMIT, mSatiatedConsumptionLimit);
545                 out.attributeLong(null, XML_ATTR_REMAINING_CONSUMABLE_CAKES,
546                         mRemainingConsumableCakes);
547                 out.endTag(null, XML_TAG_HIGH_LEVEL_STATE);
548 
549                 for (int uIdx = mLedgers.numMaps() - 1; uIdx >= 0; --uIdx) {
550                     final int userId = mLedgers.keyAt(uIdx);
551                     earliestStoredEndTime = Math.min(earliestStoredEndTime,
552                             writeUserLocked(out, userId));
553                 }
554 
555                 List<Analyst.Report> reports = mAnalyst.getReports();
556                 for (int i = 0, size = reports.size(); i < size; ++i) {
557                     writeReport(out, reports.get(i));
558                 }
559 
560                 out.endTag(null, XML_TAG_TARE);
561 
562                 out.endDocument();
563                 mStateFile.finishWrite(fos);
564             } catch (IOException e) {
565                 Slog.e(TAG, "Error writing state to disk", e);
566             }
567             scheduleCleanup(earliestStoredEndTime);
568         }
569     }
570 
571     @GuardedBy("mIrs.getLock()")
writeUserLocked(@onNull TypedXmlSerializer out, final int userId)572     private long writeUserLocked(@NonNull TypedXmlSerializer out, final int userId)
573             throws IOException {
574         final int uIdx = mLedgers.indexOfKey(userId);
575         long earliestStoredEndTime = Long.MAX_VALUE;
576 
577         out.startTag(null, XML_TAG_USER);
578         out.attributeInt(null, XML_ATTR_USER_ID, userId);
579         for (int pIdx = mLedgers.numElementsForKey(userId) - 1; pIdx >= 0; --pIdx) {
580             final String pkgName = mLedgers.keyAt(uIdx, pIdx);
581             final Ledger ledger = mLedgers.get(userId, pkgName);
582             // Remove old transactions so we don't waste space storing them.
583             ledger.removeOldTransactions(MAX_TRANSACTION_AGE_MS);
584 
585             out.startTag(null, XML_TAG_LEDGER);
586             out.attribute(null, XML_ATTR_PACKAGE_NAME, pkgName);
587             out.attributeLong(null,
588                     XML_ATTR_CURRENT_BALANCE, ledger.getCurrentBalance());
589 
590             final List<Ledger.Transaction> transactions = ledger.getTransactions();
591             for (int t = 0; t < transactions.size(); ++t) {
592                 Ledger.Transaction transaction = transactions.get(t);
593                 if (t == 0) {
594                     earliestStoredEndTime = Math.min(earliestStoredEndTime, transaction.endTimeMs);
595                 }
596                 writeTransaction(out, transaction);
597             }
598             out.endTag(null, XML_TAG_LEDGER);
599         }
600         out.endTag(null, XML_TAG_USER);
601 
602         return earliestStoredEndTime;
603     }
604 
writeTransaction(@onNull TypedXmlSerializer out, @NonNull Ledger.Transaction transaction)605     private static void writeTransaction(@NonNull TypedXmlSerializer out,
606             @NonNull Ledger.Transaction transaction) throws IOException {
607         out.startTag(null, XML_TAG_TRANSACTION);
608         out.attributeLong(null, XML_ATTR_START_TIME, transaction.startTimeMs);
609         out.attributeLong(null, XML_ATTR_END_TIME, transaction.endTimeMs);
610         out.attributeInt(null, XML_ATTR_EVENT_ID, transaction.eventId);
611         if (transaction.tag != null) {
612             out.attribute(null, XML_ATTR_TAG, transaction.tag);
613         }
614         out.attributeLong(null, XML_ATTR_DELTA, transaction.delta);
615         out.attributeLong(null, XML_ATTR_CTP, transaction.ctp);
616         out.endTag(null, XML_TAG_TRANSACTION);
617     }
618 
writeReport(@onNull TypedXmlSerializer out, @NonNull Analyst.Report report)619     private static void writeReport(@NonNull TypedXmlSerializer out,
620             @NonNull Analyst.Report report) throws IOException {
621         out.startTag(null, XML_TAG_PERIOD_REPORT);
622         out.attributeInt(null, XML_ATTR_PR_DISCHARGE, report.cumulativeBatteryDischarge);
623         out.attributeInt(null, XML_ATTR_PR_BATTERY_LEVEL, report.currentBatteryLevel);
624         out.attributeLong(null, XML_ATTR_PR_PROFIT, report.cumulativeProfit);
625         out.attributeInt(null, XML_ATTR_PR_NUM_PROFIT, report.numProfitableActions);
626         out.attributeLong(null, XML_ATTR_PR_LOSS, report.cumulativeLoss);
627         out.attributeInt(null, XML_ATTR_PR_NUM_LOSS, report.numUnprofitableActions);
628         out.attributeLong(null, XML_ATTR_PR_REWARDS, report.cumulativeRewards);
629         out.attributeInt(null, XML_ATTR_PR_NUM_REWARDS, report.numRewards);
630         out.attributeLong(null, XML_ATTR_PR_POS_REGULATIONS, report.cumulativePositiveRegulations);
631         out.attributeInt(null, XML_ATTR_PR_NUM_POS_REGULATIONS, report.numPositiveRegulations);
632         out.attributeLong(null, XML_ATTR_PR_NEG_REGULATIONS, report.cumulativeNegativeRegulations);
633         out.attributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS, report.numNegativeRegulations);
634         out.endTag(null, XML_TAG_PERIOD_REPORT);
635     }
636 
637     @GuardedBy("mIrs.getLock()")
dumpLocked(IndentingPrintWriter pw, boolean dumpAll)638     void dumpLocked(IndentingPrintWriter pw, boolean dumpAll) {
639         pw.println("Ledgers:");
640         pw.increaseIndent();
641         mLedgers.forEach((userId, pkgName, ledger) -> {
642             pw.print(appToString(userId, pkgName));
643             if (mIrs.isSystem(userId, pkgName)) {
644                 pw.print(" (system)");
645             }
646             pw.println();
647             pw.increaseIndent();
648             ledger.dump(pw, dumpAll ? Integer.MAX_VALUE : MAX_NUM_TRANSACTION_DUMP);
649             pw.decreaseIndent();
650         });
651         pw.decreaseIndent();
652     }
653 }
654