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