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