1 /** 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations 14 * under the License. 15 */ 16 17 package com.android.server.usage; 18 19 import android.app.usage.TimeSparseArray; 20 import android.app.usage.UsageEvents; 21 import android.app.usage.UsageStats; 22 import android.app.usage.UsageStatsManager; 23 import android.os.Build; 24 import android.os.SystemProperties; 25 import android.util.ArrayMap; 26 import android.util.AtomicFile; 27 import android.util.Slog; 28 import android.util.SparseArray; 29 import android.util.TimeUtils; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.internal.util.ArrayUtils; 33 import com.android.internal.util.IndentingPrintWriter; 34 35 import libcore.io.IoUtils; 36 37 import java.io.BufferedReader; 38 import java.io.BufferedWriter; 39 import java.io.ByteArrayInputStream; 40 import java.io.ByteArrayOutputStream; 41 import java.io.DataInputStream; 42 import java.io.DataOutputStream; 43 import java.io.File; 44 import java.io.FileInputStream; 45 import java.io.FileNotFoundException; 46 import java.io.FileOutputStream; 47 import java.io.FileReader; 48 import java.io.FileWriter; 49 import java.io.FilenameFilter; 50 import java.io.IOException; 51 import java.io.InputStream; 52 import java.io.OutputStream; 53 import java.nio.file.Files; 54 import java.nio.file.StandardCopyOption; 55 import java.util.ArrayList; 56 import java.util.Arrays; 57 import java.util.HashMap; 58 import java.util.List; 59 60 /** 61 * Provides an interface to query for UsageStat data from a Protocol Buffer database. 62 * 63 * Prior to version 4, UsageStatsDatabase used XML to store Usage Stats data to disk. 64 * When the UsageStatsDatabase version is upgraded, the files on disk are migrated to the new 65 * version on init. The steps of migration are as follows: 66 * 1) Check if version upgrade breadcrumb exists on disk, if so skip to step 4. 67 * 2) Move current files to a timestamped backup directory. 68 * 3) Write a temporary breadcrumb file with some info about the backup directory. 69 * 4) Deserialize the backup files in the timestamped backup folder referenced by the breadcrumb. 70 * 5) Reserialize the data read from the file with the new version format and replace the old files 71 * 6) Repeat Step 3 and 4 for each file in the backup folder. 72 * 7) Update the version file with the new version and build fingerprint. 73 * 8) Delete the time stamped backup folder (unless flagged to be kept). 74 * 9) Delete the breadcrumb file. 75 * 76 * Performing the upgrade steps in this order, protects against unexpected shutdowns mid upgrade 77 * 78 * The backup directory will contain directories with timestamp names. If the upgrade breadcrumb 79 * exists on disk, it will contain a timestamp which will match one of the backup directories. The 80 * breadcrumb will also contain a version number which will denote how the files in the backup 81 * directory should be deserialized. 82 */ 83 public class UsageStatsDatabase { 84 private static final int DEFAULT_CURRENT_VERSION = 5; 85 /** 86 * Current version of the backup schema 87 * 88 * @hide 89 */ 90 @VisibleForTesting 91 public static final int BACKUP_VERSION = 4; 92 93 @VisibleForTesting 94 static final int[] MAX_FILES_PER_INTERVAL_TYPE = new int[]{100, 50, 12, 10}; 95 96 // Key under which the payload blob is stored 97 // same as UsageStatsBackupHelper.KEY_USAGE_STATS 98 static final String KEY_USAGE_STATS = "usage_stats"; 99 100 // Persist versioned backup files. 101 // Should be false, except when testing new versions 102 static final boolean KEEP_BACKUP_DIR = false; 103 104 private static final String TAG = "UsageStatsDatabase"; 105 private static final boolean DEBUG = UsageStatsService.DEBUG; 106 private static final String BAK_SUFFIX = ".bak"; 107 private static final String CHECKED_IN_SUFFIX = UsageStatsXml.CHECKED_IN_SUFFIX; 108 private static final String RETENTION_LEN_KEY = "ro.usagestats.chooser.retention"; 109 private static final int SELECTION_LOG_RETENTION_LEN = 110 SystemProperties.getInt(RETENTION_LEN_KEY, 14); 111 112 private final Object mLock = new Object(); 113 private final File[] mIntervalDirs; 114 @VisibleForTesting 115 final TimeSparseArray<AtomicFile>[] mSortedStatFiles; 116 private final UnixCalendar mCal; 117 private final File mVersionFile; 118 private final File mBackupsDir; 119 // If this file exists on disk, UsageStatsDatabase is in the middle of migrating files to a new 120 // version. If this file exists on boot, the upgrade was interrupted and needs to be picked up 121 // where it left off. 122 private final File mUpdateBreadcrumb; 123 // Current version of the database files schema 124 private int mCurrentVersion; 125 private boolean mFirstUpdate; 126 private boolean mNewUpdate; 127 private boolean mUpgradePerformed; 128 129 // The obfuscated packages to tokens mappings file 130 private final File mPackageMappingsFile; 131 // Holds all of the data related to the obfuscated packages and their token mappings. 132 final PackagesTokenData mPackagesTokenData = new PackagesTokenData(); 133 134 /** 135 * UsageStatsDatabase constructor that allows setting the version number. 136 * This should only be used for testing. 137 * 138 * @hide 139 */ 140 @VisibleForTesting UsageStatsDatabase(File dir, int version)141 public UsageStatsDatabase(File dir, int version) { 142 mIntervalDirs = new File[]{ 143 new File(dir, "daily"), 144 new File(dir, "weekly"), 145 new File(dir, "monthly"), 146 new File(dir, "yearly"), 147 }; 148 mCurrentVersion = version; 149 mVersionFile = new File(dir, "version"); 150 mBackupsDir = new File(dir, "backups"); 151 mUpdateBreadcrumb = new File(dir, "breadcrumb"); 152 mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length]; 153 mPackageMappingsFile = new File(dir, "mappings"); 154 mCal = new UnixCalendar(0); 155 } 156 UsageStatsDatabase(File dir)157 public UsageStatsDatabase(File dir) { 158 this(dir, DEFAULT_CURRENT_VERSION); 159 } 160 161 /** 162 * Initialize any directories required and index what stats are available. 163 */ init(long currentTimeMillis)164 public void init(long currentTimeMillis) { 165 synchronized (mLock) { 166 for (File f : mIntervalDirs) { 167 f.mkdirs(); 168 if (!f.exists()) { 169 throw new IllegalStateException("Failed to create directory " 170 + f.getAbsolutePath()); 171 } 172 } 173 174 checkVersionAndBuildLocked(); 175 indexFilesLocked(); 176 177 // Delete files that are in the future. 178 for (TimeSparseArray<AtomicFile> files : mSortedStatFiles) { 179 final int startIndex = files.closestIndexOnOrAfter(currentTimeMillis); 180 if (startIndex < 0) { 181 continue; 182 } 183 184 final int fileCount = files.size(); 185 for (int i = startIndex; i < fileCount; i++) { 186 files.valueAt(i).delete(); 187 } 188 189 // Remove in a separate loop because any accesses (valueAt) 190 // will cause a gc in the SparseArray and mess up the order. 191 for (int i = startIndex; i < fileCount; i++) { 192 files.removeAt(i); 193 } 194 } 195 } 196 } 197 198 public interface CheckinAction { checkin(IntervalStats stats)199 boolean checkin(IntervalStats stats); 200 } 201 202 /** 203 * Calls {@link CheckinAction#checkin(IntervalStats)} on the given {@link CheckinAction} 204 * for all {@link IntervalStats} that haven't been checked-in. 205 * If any of the calls to {@link CheckinAction#checkin(IntervalStats)} returns false or throws 206 * an exception, the check-in will be aborted. 207 * 208 * @param checkinAction The callback to run when checking-in {@link IntervalStats}. 209 * @return true if the check-in succeeded. 210 */ checkinDailyFiles(CheckinAction checkinAction)211 public boolean checkinDailyFiles(CheckinAction checkinAction) { 212 synchronized (mLock) { 213 final TimeSparseArray<AtomicFile> files = 214 mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY]; 215 final int fileCount = files.size(); 216 217 // We may have holes in the checkin (if there was an error) 218 // so find the last checked-in file and go from there. 219 int lastCheckin = -1; 220 for (int i = 0; i < fileCount - 1; i++) { 221 if (files.valueAt(i).getBaseFile().getPath().endsWith(CHECKED_IN_SUFFIX)) { 222 lastCheckin = i; 223 } 224 } 225 226 final int start = lastCheckin + 1; 227 if (start == fileCount - 1) { 228 return true; 229 } 230 231 try { 232 for (int i = start; i < fileCount - 1; i++) { 233 final IntervalStats stats = new IntervalStats(); 234 readLocked(files.valueAt(i), stats); 235 if (!checkinAction.checkin(stats)) { 236 return false; 237 } 238 } 239 } catch (Exception e) { 240 Slog.e(TAG, "Failed to check-in", e); 241 return false; 242 } 243 244 // We have successfully checked-in the stats, so rename the files so that they 245 // are marked as checked-in. 246 for (int i = start; i < fileCount - 1; i++) { 247 final AtomicFile file = files.valueAt(i); 248 final File checkedInFile = new File( 249 file.getBaseFile().getPath() + CHECKED_IN_SUFFIX); 250 if (!file.getBaseFile().renameTo(checkedInFile)) { 251 // We must return success, as we've already marked some files as checked-in. 252 // It's better to repeat ourselves than to lose data. 253 Slog.e(TAG, "Failed to mark file " + file.getBaseFile().getPath() 254 + " as checked-in"); 255 return true; 256 } 257 258 // AtomicFile needs to set a new backup path with the same -c extension, so 259 // we replace the old AtomicFile with the updated one. 260 files.setValueAt(i, new AtomicFile(checkedInFile)); 261 } 262 } 263 return true; 264 } 265 266 /** @hide */ 267 @VisibleForTesting forceIndexFiles()268 void forceIndexFiles() { 269 synchronized (mLock) { 270 indexFilesLocked(); 271 } 272 } 273 indexFilesLocked()274 private void indexFilesLocked() { 275 final FilenameFilter backupFileFilter = new FilenameFilter() { 276 @Override 277 public boolean accept(File dir, String name) { 278 return !name.endsWith(BAK_SUFFIX); 279 } 280 }; 281 // Index the available usage stat files on disk. 282 for (int i = 0; i < mSortedStatFiles.length; i++) { 283 if (mSortedStatFiles[i] == null) { 284 mSortedStatFiles[i] = new TimeSparseArray<>(); 285 } else { 286 mSortedStatFiles[i].clear(); 287 } 288 File[] files = mIntervalDirs[i].listFiles(backupFileFilter); 289 if (files != null) { 290 if (DEBUG) { 291 Slog.d(TAG, "Found " + files.length + " stat files for interval " + i); 292 } 293 final int len = files.length; 294 for (int j = 0; j < len; j++) { 295 final File f = files[j]; 296 final AtomicFile af = new AtomicFile(f); 297 try { 298 mSortedStatFiles[i].put(parseBeginTime(af), af); 299 } catch (IOException e) { 300 Slog.e(TAG, "failed to index file: " + f, e); 301 } 302 } 303 304 // only keep the max allowed number of files for each interval type. 305 final int toDelete = mSortedStatFiles[i].size() - MAX_FILES_PER_INTERVAL_TYPE[i]; 306 if (toDelete > 0) { 307 for (int j = 0; j < toDelete; j++) { 308 mSortedStatFiles[i].valueAt(0).delete(); 309 mSortedStatFiles[i].removeAt(0); 310 } 311 Slog.d(TAG, "Deleted " + toDelete + " stat files for interval " + i); 312 } 313 } 314 } 315 } 316 317 /** 318 * Is this the first update to the system from L to M? 319 */ isFirstUpdate()320 boolean isFirstUpdate() { 321 return mFirstUpdate; 322 } 323 324 /** 325 * Is this a system update since we started tracking build fingerprint in the version file? 326 */ isNewUpdate()327 boolean isNewUpdate() { 328 return mNewUpdate; 329 } 330 331 /** 332 * Was an upgrade performed when this database was initialized? 333 */ wasUpgradePerformed()334 boolean wasUpgradePerformed() { 335 return mUpgradePerformed; 336 } 337 checkVersionAndBuildLocked()338 private void checkVersionAndBuildLocked() { 339 int version; 340 String buildFingerprint; 341 String currentFingerprint = getBuildFingerprint(); 342 mFirstUpdate = true; 343 mNewUpdate = true; 344 try (BufferedReader reader = new BufferedReader(new FileReader(mVersionFile))) { 345 version = Integer.parseInt(reader.readLine()); 346 buildFingerprint = reader.readLine(); 347 if (buildFingerprint != null) { 348 mFirstUpdate = false; 349 } 350 if (currentFingerprint.equals(buildFingerprint)) { 351 mNewUpdate = false; 352 } 353 } catch (NumberFormatException | IOException e) { 354 version = 0; 355 } 356 357 if (version != mCurrentVersion) { 358 Slog.i(TAG, "Upgrading from version " + version + " to " + mCurrentVersion); 359 if (!mUpdateBreadcrumb.exists()) { 360 try { 361 doUpgradeLocked(version); 362 } catch (Exception e) { 363 Slog.e(TAG, 364 "Failed to upgrade from version " + version + " to " + mCurrentVersion, 365 e); 366 // Fallback to previous version. 367 mCurrentVersion = version; 368 return; 369 } 370 } else { 371 Slog.i(TAG, "Version upgrade breadcrumb found on disk! Continuing version upgrade"); 372 } 373 } 374 375 if (mUpdateBreadcrumb.exists()) { 376 int previousVersion; 377 long token; 378 try (BufferedReader reader = new BufferedReader( 379 new FileReader(mUpdateBreadcrumb))) { 380 token = Long.parseLong(reader.readLine()); 381 previousVersion = Integer.parseInt(reader.readLine()); 382 } catch (NumberFormatException | IOException e) { 383 Slog.e(TAG, "Failed read version upgrade breadcrumb"); 384 throw new RuntimeException(e); 385 } 386 if (mCurrentVersion >= 4) { 387 continueUpgradeLocked(previousVersion, token); 388 } else { 389 Slog.wtf(TAG, "Attempting to upgrade to an unsupported version: " 390 + mCurrentVersion); 391 } 392 } 393 394 if (version != mCurrentVersion || mNewUpdate) { 395 try (BufferedWriter writer = new BufferedWriter(new FileWriter(mVersionFile))) { 396 writer.write(Integer.toString(mCurrentVersion)); 397 writer.write("\n"); 398 writer.write(currentFingerprint); 399 writer.write("\n"); 400 writer.flush(); 401 } catch (IOException e) { 402 Slog.e(TAG, "Failed to write new version"); 403 throw new RuntimeException(e); 404 } 405 } 406 407 if (mUpdateBreadcrumb.exists()) { 408 // Files should be up to date with current version. Clear the version update breadcrumb 409 mUpdateBreadcrumb.delete(); 410 // update mUpgradePerformed after breadcrumb is deleted to indicate a successful upgrade 411 mUpgradePerformed = true; 412 } 413 414 if (mBackupsDir.exists() && !KEEP_BACKUP_DIR) { 415 mUpgradePerformed = true; // updated here to ensure that data is cleaned up 416 deleteDirectory(mBackupsDir); 417 } 418 } 419 getBuildFingerprint()420 private String getBuildFingerprint() { 421 return Build.VERSION.RELEASE + ";" 422 + Build.VERSION.CODENAME + ";" 423 + Build.VERSION.INCREMENTAL; 424 } 425 doUpgradeLocked(int thisVersion)426 private void doUpgradeLocked(int thisVersion) { 427 if (thisVersion < 2) { 428 // Delete all files if we are version 0. This is a pre-release version, 429 // so this is fine. 430 Slog.i(TAG, "Deleting all usage stats files"); 431 for (int i = 0; i < mIntervalDirs.length; i++) { 432 File[] files = mIntervalDirs[i].listFiles(); 433 if (files != null) { 434 for (File f : files) { 435 f.delete(); 436 } 437 } 438 } 439 } else { 440 // Create a dir in backups based on current timestamp 441 final long token = System.currentTimeMillis(); 442 final File backupDir = new File(mBackupsDir, Long.toString(token)); 443 backupDir.mkdirs(); 444 if (!backupDir.exists()) { 445 throw new IllegalStateException( 446 "Failed to create backup directory " + backupDir.getAbsolutePath()); 447 } 448 try { 449 Files.copy(mVersionFile.toPath(), 450 new File(backupDir, mVersionFile.getName()).toPath(), 451 StandardCopyOption.REPLACE_EXISTING); 452 } catch (IOException e) { 453 Slog.e(TAG, "Failed to back up version file : " + mVersionFile.toString()); 454 throw new RuntimeException(e); 455 } 456 457 for (int i = 0; i < mIntervalDirs.length; i++) { 458 final File backupIntervalDir = new File(backupDir, mIntervalDirs[i].getName()); 459 backupIntervalDir.mkdir(); 460 461 if (!backupIntervalDir.exists()) { 462 throw new IllegalStateException( 463 "Failed to create interval backup directory " 464 + backupIntervalDir.getAbsolutePath()); 465 } 466 File[] files = mIntervalDirs[i].listFiles(); 467 if (files != null) { 468 for (int j = 0; j < files.length; j++) { 469 final File backupFile = new File(backupIntervalDir, files[j].getName()); 470 if (DEBUG) { 471 Slog.d(TAG, "Creating versioned (" + Integer.toString(thisVersion) 472 + ") backup of " + files[j].toString() 473 + " stat files for interval " 474 + i + " to " + backupFile.toString()); 475 } 476 477 try { 478 // Backup file should not already exist, but make sure it doesn't 479 Files.move(files[j].toPath(), backupFile.toPath(), 480 StandardCopyOption.REPLACE_EXISTING); 481 } catch (IOException e) { 482 Slog.e(TAG, "Failed to back up file : " + files[j].toString()); 483 throw new RuntimeException(e); 484 } 485 } 486 } 487 } 488 489 // Leave a breadcrumb behind noting that all the usage stats have been moved to a backup 490 BufferedWriter writer = null; 491 try { 492 writer = new BufferedWriter(new FileWriter(mUpdateBreadcrumb)); 493 writer.write(Long.toString(token)); 494 writer.write("\n"); 495 writer.write(Integer.toString(thisVersion)); 496 writer.write("\n"); 497 writer.flush(); 498 } catch (IOException e) { 499 Slog.e(TAG, "Failed to write new version upgrade breadcrumb"); 500 throw new RuntimeException(e); 501 } finally { 502 IoUtils.closeQuietly(writer); 503 } 504 } 505 } 506 continueUpgradeLocked(int version, long token)507 private void continueUpgradeLocked(int version, long token) { 508 if (version <= 3) { 509 Slog.w(TAG, "Reading UsageStats as XML; current database version: " + mCurrentVersion); 510 } 511 final File backupDir = new File(mBackupsDir, Long.toString(token)); 512 513 // Upgrade step logic for the entire usage stats directory, not individual interval dirs. 514 if (version >= 5) { 515 readMappingsLocked(); 516 } 517 518 // Read each file in the backup according to the version and write to the interval 519 // directories in the current versions format 520 for (int i = 0; i < mIntervalDirs.length; i++) { 521 final File backedUpInterval = new File(backupDir, mIntervalDirs[i].getName()); 522 File[] files = backedUpInterval.listFiles(); 523 if (files != null) { 524 for (int j = 0; j < files.length; j++) { 525 if (DEBUG) { 526 Slog.d(TAG, 527 "Upgrading " + files[j].toString() + " to version (" 528 + Integer.toString( 529 mCurrentVersion) + ") for interval " + i); 530 } 531 try { 532 IntervalStats stats = new IntervalStats(); 533 readLocked(new AtomicFile(files[j]), stats, version, mPackagesTokenData); 534 // Upgrade to version 5+. 535 // Future version upgrades should add additional logic here to upgrade. 536 if (mCurrentVersion >= 5) { 537 // Create the initial obfuscated packages map. 538 stats.obfuscateData(mPackagesTokenData); 539 } 540 writeLocked(new AtomicFile(new File(mIntervalDirs[i], 541 Long.toString(stats.beginTime))), stats, mCurrentVersion, 542 mPackagesTokenData); 543 } catch (Exception e) { 544 // This method is called on boot, log the exception and move on 545 Slog.e(TAG, "Failed to upgrade backup file : " + files[j].toString()); 546 } 547 } 548 } 549 } 550 551 // Upgrade step logic for the entire usage stats directory, not individual interval dirs. 552 if (mCurrentVersion >= 5) { 553 try { 554 writeMappingsLocked(); 555 } catch (IOException e) { 556 Slog.e(TAG, "Failed to write the tokens mappings file."); 557 } 558 } 559 } 560 561 /** 562 * Returns the token mapped to the package removed or {@code PackagesTokenData.UNASSIGNED_TOKEN} 563 * if not mapped. 564 */ onPackageRemoved(String packageName, long timeRemoved)565 int onPackageRemoved(String packageName, long timeRemoved) { 566 synchronized (mLock) { 567 final int tokenRemoved = mPackagesTokenData.removePackage(packageName, timeRemoved); 568 try { 569 writeMappingsLocked(); 570 } catch (Exception e) { 571 Slog.w(TAG, "Unable to update package mappings on disk after removing token " 572 + tokenRemoved); 573 } 574 return tokenRemoved; 575 } 576 } 577 578 /** 579 * Reads all the usage stats data on disk and rewrites it with any data related to uninstalled 580 * packages omitted. Returns {@code true} on success, {@code false} otherwise. 581 */ pruneUninstalledPackagesData()582 boolean pruneUninstalledPackagesData() { 583 synchronized (mLock) { 584 for (int i = 0; i < mIntervalDirs.length; i++) { 585 final File[] files = mIntervalDirs[i].listFiles(); 586 if (files == null) { 587 continue; 588 } 589 for (int j = 0; j < files.length; j++) { 590 try { 591 final IntervalStats stats = new IntervalStats(); 592 final AtomicFile atomicFile = new AtomicFile(files[j]); 593 if (!readLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData)) { 594 continue; // no data was omitted when read so no need to rewrite 595 } 596 // Any data related to packages that have been removed would have failed 597 // the deobfuscation step on read so the IntervalStats object here only 598 // contains data for packages that are currently installed - all we need 599 // to do here is write the data back to disk. 600 writeLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData); 601 } catch (Exception e) { 602 Slog.e(TAG, "Failed to prune data from: " + files[j].toString()); 603 return false; 604 } 605 } 606 } 607 608 try { 609 writeMappingsLocked(); 610 } catch (IOException e) { 611 Slog.e(TAG, "Failed to write package mappings after pruning data."); 612 return false; 613 } 614 return true; 615 } 616 } 617 618 /** 619 * Iterates through all the files on disk and prunes any data that belongs to packages that have 620 * been uninstalled (packages that are not in the given list). 621 * Note: this should only be called once, when there has been a database upgrade. 622 * 623 * @param installedPackages map of installed packages (package_name:package_install_time) 624 */ prunePackagesDataOnUpgrade(HashMap<String, Long> installedPackages)625 void prunePackagesDataOnUpgrade(HashMap<String, Long> installedPackages) { 626 if (ArrayUtils.isEmpty(installedPackages)) { 627 return; 628 } 629 synchronized (mLock) { 630 for (int i = 0; i < mIntervalDirs.length; i++) { 631 final File[] files = mIntervalDirs[i].listFiles(); 632 if (files == null) { 633 continue; 634 } 635 for (int j = 0; j < files.length; j++) { 636 try { 637 final IntervalStats stats = new IntervalStats(); 638 final AtomicFile atomicFile = new AtomicFile(files[j]); 639 readLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData); 640 if (!pruneStats(installedPackages, stats)) { 641 continue; // no stats were pruned so no need to rewrite 642 } 643 writeLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData); 644 } catch (Exception e) { 645 Slog.e(TAG, "Failed to prune data from: " + files[j].toString()); 646 } 647 } 648 } 649 } 650 } 651 pruneStats(HashMap<String, Long> installedPackages, IntervalStats stats)652 private boolean pruneStats(HashMap<String, Long> installedPackages, IntervalStats stats) { 653 boolean dataPruned = false; 654 655 // prune old package usage stats 656 for (int i = stats.packageStats.size() - 1; i >= 0; i--) { 657 final UsageStats usageStats = stats.packageStats.valueAt(i); 658 final Long timeInstalled = installedPackages.get(usageStats.mPackageName); 659 if (timeInstalled == null || timeInstalled > usageStats.mEndTimeStamp) { 660 stats.packageStats.removeAt(i); 661 dataPruned = true; 662 } 663 } 664 if (dataPruned) { 665 // ensure old stats don't linger around during the obfuscation step on write 666 stats.packageStatsObfuscated.clear(); 667 } 668 669 // prune old events 670 for (int i = stats.events.size() - 1; i >= 0; i--) { 671 final UsageEvents.Event event = stats.events.get(i); 672 final Long timeInstalled = installedPackages.get(event.mPackage); 673 if (timeInstalled == null || timeInstalled > event.mTimeStamp) { 674 stats.events.remove(i); 675 dataPruned = true; 676 } 677 } 678 679 return dataPruned; 680 } 681 onTimeChanged(long timeDiffMillis)682 public void onTimeChanged(long timeDiffMillis) { 683 synchronized (mLock) { 684 StringBuilder logBuilder = new StringBuilder(); 685 logBuilder.append("Time changed by "); 686 TimeUtils.formatDuration(timeDiffMillis, logBuilder); 687 logBuilder.append("."); 688 689 int filesDeleted = 0; 690 int filesMoved = 0; 691 692 for (TimeSparseArray<AtomicFile> files : mSortedStatFiles) { 693 final int fileCount = files.size(); 694 for (int i = 0; i < fileCount; i++) { 695 final AtomicFile file = files.valueAt(i); 696 final long newTime = files.keyAt(i) + timeDiffMillis; 697 if (newTime < 0) { 698 filesDeleted++; 699 file.delete(); 700 } else { 701 try { 702 file.openRead().close(); 703 } catch (IOException e) { 704 // Ignore, this is just to make sure there are no backups. 705 } 706 707 String newName = Long.toString(newTime); 708 if (file.getBaseFile().getName().endsWith(CHECKED_IN_SUFFIX)) { 709 newName = newName + CHECKED_IN_SUFFIX; 710 } 711 712 final File newFile = new File(file.getBaseFile().getParentFile(), newName); 713 filesMoved++; 714 file.getBaseFile().renameTo(newFile); 715 } 716 } 717 files.clear(); 718 } 719 720 logBuilder.append(" files deleted: ").append(filesDeleted); 721 logBuilder.append(" files moved: ").append(filesMoved); 722 Slog.i(TAG, logBuilder.toString()); 723 724 // Now re-index the new files. 725 indexFilesLocked(); 726 } 727 } 728 729 /** 730 * Get the latest stats that exist for this interval type. 731 */ getLatestUsageStats(int intervalType)732 public IntervalStats getLatestUsageStats(int intervalType) { 733 synchronized (mLock) { 734 if (intervalType < 0 || intervalType >= mIntervalDirs.length) { 735 throw new IllegalArgumentException("Bad interval type " + intervalType); 736 } 737 738 final int fileCount = mSortedStatFiles[intervalType].size(); 739 if (fileCount == 0) { 740 return null; 741 } 742 743 try { 744 final AtomicFile f = mSortedStatFiles[intervalType].valueAt(fileCount - 1); 745 IntervalStats stats = new IntervalStats(); 746 readLocked(f, stats); 747 return stats; 748 } catch (Exception e) { 749 Slog.e(TAG, "Failed to read usage stats file", e); 750 } 751 } 752 return null; 753 } 754 755 /** 756 * Filter out those stats from the given stats that belong to removed packages. Filtering out 757 * all of the stats at once has an amortized cost for future calls. 758 */ filterStats(IntervalStats stats)759 void filterStats(IntervalStats stats) { 760 if (mPackagesTokenData.removedPackagesMap.isEmpty()) { 761 return; 762 } 763 final ArrayMap<String, Long> removedPackagesMap = mPackagesTokenData.removedPackagesMap; 764 765 // filter out package usage stats 766 final int removedPackagesSize = removedPackagesMap.size(); 767 for (int i = 0; i < removedPackagesSize; i++) { 768 final String removedPackage = removedPackagesMap.keyAt(i); 769 final UsageStats usageStats = stats.packageStats.get(removedPackage); 770 if (usageStats != null && usageStats.mEndTimeStamp < removedPackagesMap.valueAt(i)) { 771 stats.packageStats.remove(removedPackage); 772 } 773 } 774 775 // filter out events 776 for (int i = stats.events.size() - 1; i >= 0; i--) { 777 final UsageEvents.Event event = stats.events.get(i); 778 final Long timeRemoved = removedPackagesMap.get(event.mPackage); 779 if (timeRemoved != null && timeRemoved > event.mTimeStamp) { 780 stats.events.remove(i); 781 } 782 } 783 } 784 785 /** 786 * Figures out what to extract from the given IntervalStats object. 787 */ 788 public interface StatCombiner<T> { 789 790 /** 791 * Implementations should extract interesting from <code>stats</code> and add it 792 * to the <code>accumulatedResult</code> list. 793 * 794 * If the <code>stats</code> object is mutable, <code>mutable</code> will be true, 795 * which means you should make a copy of the data before adding it to the 796 * <code>accumulatedResult</code> list. 797 * 798 * @param stats The {@link IntervalStats} object selected. 799 * @param mutable Whether or not the data inside the stats object is mutable. 800 * @param accumulatedResult The list to which to add extracted data. 801 */ combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult)802 void combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult); 803 } 804 805 /** 806 * Find all {@link IntervalStats} for the given range and interval type. 807 */ queryUsageStats(int intervalType, long beginTime, long endTime, StatCombiner<T> combiner)808 public <T> List<T> queryUsageStats(int intervalType, long beginTime, long endTime, 809 StatCombiner<T> combiner) { 810 synchronized (mLock) { 811 if (intervalType < 0 || intervalType >= mIntervalDirs.length) { 812 throw new IllegalArgumentException("Bad interval type " + intervalType); 813 } 814 815 final TimeSparseArray<AtomicFile> intervalStats = mSortedStatFiles[intervalType]; 816 817 if (endTime <= beginTime) { 818 if (DEBUG) { 819 Slog.d(TAG, "endTime(" + endTime + ") <= beginTime(" + beginTime + ")"); 820 } 821 return null; 822 } 823 824 int startIndex = intervalStats.closestIndexOnOrBefore(beginTime); 825 if (startIndex < 0) { 826 // All the stats available have timestamps after beginTime, which means they all 827 // match. 828 startIndex = 0; 829 } 830 831 int endIndex = intervalStats.closestIndexOnOrBefore(endTime); 832 if (endIndex < 0) { 833 // All the stats start after this range ends, so nothing matches. 834 if (DEBUG) { 835 Slog.d(TAG, "No results for this range. All stats start after."); 836 } 837 return null; 838 } 839 840 if (intervalStats.keyAt(endIndex) == endTime) { 841 // The endTime is exclusive, so if we matched exactly take the one before. 842 endIndex--; 843 if (endIndex < 0) { 844 // All the stats start after this range ends, so nothing matches. 845 if (DEBUG) { 846 Slog.d(TAG, "No results for this range. All stats start after."); 847 } 848 return null; 849 } 850 } 851 852 final ArrayList<T> results = new ArrayList<>(); 853 for (int i = startIndex; i <= endIndex; i++) { 854 final AtomicFile f = intervalStats.valueAt(i); 855 final IntervalStats stats = new IntervalStats(); 856 857 if (DEBUG) { 858 Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath()); 859 } 860 861 try { 862 readLocked(f, stats); 863 if (beginTime < stats.endTime) { 864 combiner.combine(stats, false, results); 865 } 866 } catch (Exception e) { 867 Slog.e(TAG, "Failed to read usage stats file", e); 868 // We continue so that we return results that are not 869 // corrupt. 870 } 871 } 872 return results; 873 } 874 } 875 876 /** 877 * Find the interval that best matches this range. 878 * 879 * TODO(adamlesinski): Use endTimeStamp in best fit calculation. 880 */ findBestFitBucket(long beginTimeStamp, long endTimeStamp)881 public int findBestFitBucket(long beginTimeStamp, long endTimeStamp) { 882 synchronized (mLock) { 883 int bestBucket = -1; 884 long smallestDiff = Long.MAX_VALUE; 885 for (int i = mSortedStatFiles.length - 1; i >= 0; i--) { 886 final int index = mSortedStatFiles[i].closestIndexOnOrBefore(beginTimeStamp); 887 int size = mSortedStatFiles[i].size(); 888 if (index >= 0 && index < size) { 889 // We have some results here, check if they are better than our current match. 890 long diff = Math.abs(mSortedStatFiles[i].keyAt(index) - beginTimeStamp); 891 if (diff < smallestDiff) { 892 smallestDiff = diff; 893 bestBucket = i; 894 } 895 } 896 } 897 return bestBucket; 898 } 899 } 900 901 /** 902 * Remove any usage stat files that are too old. 903 */ prune(final long currentTimeMillis)904 public void prune(final long currentTimeMillis) { 905 synchronized (mLock) { 906 mCal.setTimeInMillis(currentTimeMillis); 907 mCal.addYears(-3); 908 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_YEARLY], 909 mCal.getTimeInMillis()); 910 911 mCal.setTimeInMillis(currentTimeMillis); 912 mCal.addMonths(-6); 913 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_MONTHLY], 914 mCal.getTimeInMillis()); 915 916 mCal.setTimeInMillis(currentTimeMillis); 917 mCal.addWeeks(-4); 918 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_WEEKLY], 919 mCal.getTimeInMillis()); 920 921 mCal.setTimeInMillis(currentTimeMillis); 922 mCal.addDays(-10); 923 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_DAILY], 924 mCal.getTimeInMillis()); 925 926 mCal.setTimeInMillis(currentTimeMillis); 927 mCal.addDays(-SELECTION_LOG_RETENTION_LEN); 928 for (int i = 0; i < mIntervalDirs.length; ++i) { 929 pruneChooserCountsOlderThan(mIntervalDirs[i], mCal.getTimeInMillis()); 930 } 931 932 // We must re-index our file list or we will be trying to read 933 // deleted files. 934 indexFilesLocked(); 935 } 936 } 937 pruneFilesOlderThan(File dir, long expiryTime)938 private static void pruneFilesOlderThan(File dir, long expiryTime) { 939 File[] files = dir.listFiles(); 940 if (files != null) { 941 for (File f : files) { 942 long beginTime; 943 try { 944 beginTime = parseBeginTime(f); 945 } catch (IOException e) { 946 beginTime = 0; 947 } 948 949 if (beginTime < expiryTime) { 950 new AtomicFile(f).delete(); 951 } 952 } 953 } 954 } 955 pruneChooserCountsOlderThan(File dir, long expiryTime)956 private void pruneChooserCountsOlderThan(File dir, long expiryTime) { 957 File[] files = dir.listFiles(); 958 if (files != null) { 959 for (File f : files) { 960 long beginTime; 961 try { 962 beginTime = parseBeginTime(f); 963 } catch (IOException e) { 964 beginTime = 0; 965 } 966 967 if (beginTime < expiryTime) { 968 try { 969 final AtomicFile af = new AtomicFile(f); 970 final IntervalStats stats = new IntervalStats(); 971 readLocked(af, stats); 972 final int pkgCount = stats.packageStats.size(); 973 for (int i = 0; i < pkgCount; i++) { 974 UsageStats pkgStats = stats.packageStats.valueAt(i); 975 if (pkgStats.mChooserCounts != null) { 976 pkgStats.mChooserCounts.clear(); 977 } 978 } 979 writeLocked(af, stats); 980 } catch (Exception e) { 981 Slog.e(TAG, "Failed to delete chooser counts from usage stats file", e); 982 } 983 } 984 } 985 } 986 } 987 988 parseBeginTime(AtomicFile file)989 private static long parseBeginTime(AtomicFile file) throws IOException { 990 return parseBeginTime(file.getBaseFile()); 991 } 992 parseBeginTime(File file)993 private static long parseBeginTime(File file) throws IOException { 994 String name = file.getName(); 995 996 // Parse out the digits from the the front of the file name 997 for (int i = 0; i < name.length(); i++) { 998 final char c = name.charAt(i); 999 if (c < '0' || c > '9') { 1000 // found first char that is not a digit. 1001 name = name.substring(0, i); 1002 break; 1003 } 1004 } 1005 1006 try { 1007 return Long.parseLong(name); 1008 } catch (NumberFormatException e) { 1009 throw new IOException(e); 1010 } 1011 } 1012 writeLocked(AtomicFile file, IntervalStats stats)1013 private void writeLocked(AtomicFile file, IntervalStats stats) 1014 throws IOException, RuntimeException { 1015 if (mCurrentVersion <= 3) { 1016 Slog.wtf(TAG, "Attempting to write UsageStats as XML with version " + mCurrentVersion); 1017 return; 1018 } 1019 writeLocked(file, stats, mCurrentVersion, mPackagesTokenData); 1020 } 1021 writeLocked(AtomicFile file, IntervalStats stats, int version, PackagesTokenData packagesTokenData)1022 private static void writeLocked(AtomicFile file, IntervalStats stats, int version, 1023 PackagesTokenData packagesTokenData) throws IOException, RuntimeException { 1024 FileOutputStream fos = file.startWrite(); 1025 try { 1026 writeLocked(fos, stats, version, packagesTokenData); 1027 file.finishWrite(fos); 1028 fos = null; 1029 } catch (Exception e) { 1030 // Do nothing. Exception has already been handled. 1031 } finally { 1032 // When fos is null (successful write), this will no-op 1033 file.failWrite(fos); 1034 } 1035 } 1036 writeLocked(OutputStream out, IntervalStats stats, int version, PackagesTokenData packagesTokenData)1037 private static void writeLocked(OutputStream out, IntervalStats stats, int version, 1038 PackagesTokenData packagesTokenData) throws Exception { 1039 switch (version) { 1040 case 1: 1041 case 2: 1042 case 3: 1043 Slog.wtf(TAG, "Attempting to write UsageStats as XML with version " + version); 1044 break; 1045 case 4: 1046 try { 1047 UsageStatsProto.write(out, stats); 1048 } catch (Exception e) { 1049 Slog.e(TAG, "Unable to write interval stats to proto.", e); 1050 throw e; 1051 } 1052 break; 1053 case 5: 1054 stats.obfuscateData(packagesTokenData); 1055 try { 1056 UsageStatsProtoV2.write(out, stats); 1057 } catch (Exception e) { 1058 Slog.e(TAG, "Unable to write interval stats to proto.", e); 1059 throw e; 1060 } 1061 break; 1062 default: 1063 throw new RuntimeException( 1064 "Unhandled UsageStatsDatabase version: " + Integer.toString(version) 1065 + " on write."); 1066 } 1067 } 1068 1069 /** 1070 * Note: the data read from the given file will add to the IntervalStats object passed into this 1071 * method. It is up to the caller to ensure that this is the desired behavior - if not, the 1072 * caller should ensure that the data in the reused object is being cleared. 1073 */ readLocked(AtomicFile file, IntervalStats statsOut)1074 private void readLocked(AtomicFile file, IntervalStats statsOut) 1075 throws IOException, RuntimeException { 1076 if (mCurrentVersion <= 3) { 1077 Slog.wtf(TAG, "Reading UsageStats as XML; current database version: " 1078 + mCurrentVersion); 1079 } 1080 readLocked(file, statsOut, mCurrentVersion, mPackagesTokenData); 1081 } 1082 1083 /** 1084 * Returns {@code true} if any stats were omitted while reading, {@code false} otherwise. 1085 * <p/> 1086 * Note: the data read from the given file will add to the IntervalStats object passed into this 1087 * method. It is up to the caller to ensure that this is the desired behavior - if not, the 1088 * caller should ensure that the data in the reused object is being cleared. 1089 */ readLocked(AtomicFile file, IntervalStats statsOut, int version, PackagesTokenData packagesTokenData)1090 private static boolean readLocked(AtomicFile file, IntervalStats statsOut, int version, 1091 PackagesTokenData packagesTokenData) throws IOException, RuntimeException { 1092 boolean dataOmitted = false; 1093 try { 1094 FileInputStream in = file.openRead(); 1095 try { 1096 statsOut.beginTime = parseBeginTime(file); 1097 dataOmitted = readLocked(in, statsOut, version, packagesTokenData); 1098 statsOut.lastTimeSaved = file.getLastModifiedTime(); 1099 } finally { 1100 try { 1101 in.close(); 1102 } catch (IOException e) { 1103 // Empty 1104 } 1105 } 1106 } catch (FileNotFoundException e) { 1107 Slog.e(TAG, "UsageStatsDatabase", e); 1108 throw e; 1109 } 1110 return dataOmitted; 1111 } 1112 1113 /** 1114 * Returns {@code true} if any stats were omitted while reading, {@code false} otherwise. 1115 * <p/> 1116 * Note: the data read from the given file will add to the IntervalStats object passed into this 1117 * method. It is up to the caller to ensure that this is the desired behavior - if not, the 1118 * caller should ensure that the data in the reused object is being cleared. 1119 */ readLocked(InputStream in, IntervalStats statsOut, int version, PackagesTokenData packagesTokenData)1120 private static boolean readLocked(InputStream in, IntervalStats statsOut, int version, 1121 PackagesTokenData packagesTokenData) throws RuntimeException { 1122 boolean dataOmitted = false; 1123 switch (version) { 1124 case 1: 1125 case 2: 1126 case 3: 1127 Slog.w(TAG, "Reading UsageStats as XML; database version: " + version); 1128 try { 1129 UsageStatsXml.read(in, statsOut); 1130 } catch (Exception e) { 1131 Slog.e(TAG, "Unable to read interval stats from XML", e); 1132 } 1133 break; 1134 case 4: 1135 try { 1136 UsageStatsProto.read(in, statsOut); 1137 } catch (Exception e) { 1138 Slog.e(TAG, "Unable to read interval stats from proto.", e); 1139 } 1140 break; 1141 case 5: 1142 try { 1143 UsageStatsProtoV2.read(in, statsOut); 1144 } catch (Exception e) { 1145 Slog.e(TAG, "Unable to read interval stats from proto.", e); 1146 } 1147 dataOmitted = statsOut.deobfuscateData(packagesTokenData); 1148 break; 1149 default: 1150 throw new RuntimeException( 1151 "Unhandled UsageStatsDatabase version: " + Integer.toString(version) 1152 + " on read."); 1153 } 1154 return dataOmitted; 1155 } 1156 1157 /** 1158 * Reads the obfuscated data file from disk containing the tokens to packages mappings and 1159 * rebuilds the packages to tokens mappings based on that data. 1160 */ readMappingsLocked()1161 public void readMappingsLocked() { 1162 if (!mPackageMappingsFile.exists()) { 1163 return; // package mappings file is missing - recreate mappings on next write. 1164 } 1165 1166 try (FileInputStream in = new AtomicFile(mPackageMappingsFile).openRead()) { 1167 UsageStatsProtoV2.readObfuscatedData(in, mPackagesTokenData); 1168 } catch (Exception e) { 1169 Slog.e(TAG, "Failed to read the obfuscated packages mapping file.", e); 1170 return; 1171 } 1172 1173 final SparseArray<ArrayList<String>> tokensToPackagesMap = 1174 mPackagesTokenData.tokensToPackagesMap; 1175 final int tokensToPackagesMapSize = tokensToPackagesMap.size(); 1176 for (int i = 0; i < tokensToPackagesMapSize; i++) { 1177 final int packageToken = tokensToPackagesMap.keyAt(i); 1178 final ArrayList<String> tokensMap = tokensToPackagesMap.valueAt(i); 1179 final ArrayMap<String, Integer> packageStringsMap = new ArrayMap<>(); 1180 final int tokensMapSize = tokensMap.size(); 1181 // package name will always be at index 0 but its token should not be 0 1182 packageStringsMap.put(tokensMap.get(0), packageToken); 1183 for (int j = 1; j < tokensMapSize; j++) { 1184 packageStringsMap.put(tokensMap.get(j), j); 1185 } 1186 mPackagesTokenData.packagesToTokensMap.put(tokensMap.get(0), packageStringsMap); 1187 } 1188 } 1189 writeMappingsLocked()1190 void writeMappingsLocked() throws IOException { 1191 final AtomicFile file = new AtomicFile(mPackageMappingsFile); 1192 FileOutputStream fos = file.startWrite(); 1193 try { 1194 UsageStatsProtoV2.writeObfuscatedData(fos, mPackagesTokenData); 1195 file.finishWrite(fos); 1196 fos = null; 1197 } catch (Exception e) { 1198 Slog.e(TAG, "Unable to write obfuscated data to proto.", e); 1199 } finally { 1200 file.failWrite(fos); 1201 } 1202 } 1203 obfuscateCurrentStats(IntervalStats[] currentStats)1204 void obfuscateCurrentStats(IntervalStats[] currentStats) { 1205 if (mCurrentVersion < 5) { 1206 return; 1207 } 1208 for (int i = 0; i < currentStats.length; i++) { 1209 final IntervalStats stats = currentStats[i]; 1210 stats.obfuscateData(mPackagesTokenData); 1211 } 1212 } 1213 1214 /** 1215 * Update the stats in the database. They may not be written to disk immediately. 1216 */ putUsageStats(int intervalType, IntervalStats stats)1217 public void putUsageStats(int intervalType, IntervalStats stats) throws IOException { 1218 if (stats == null) return; 1219 synchronized (mLock) { 1220 if (intervalType < 0 || intervalType >= mIntervalDirs.length) { 1221 throw new IllegalArgumentException("Bad interval type " + intervalType); 1222 } 1223 1224 AtomicFile f = mSortedStatFiles[intervalType].get(stats.beginTime); 1225 if (f == null) { 1226 f = new AtomicFile(new File(mIntervalDirs[intervalType], 1227 Long.toString(stats.beginTime))); 1228 mSortedStatFiles[intervalType].put(stats.beginTime, f); 1229 } 1230 1231 writeLocked(f, stats); 1232 stats.lastTimeSaved = f.getLastModifiedTime(); 1233 } 1234 } 1235 1236 1237 /* Backup/Restore Code */ getBackupPayload(String key)1238 byte[] getBackupPayload(String key) { 1239 return getBackupPayload(key, BACKUP_VERSION); 1240 } 1241 1242 /** 1243 * @hide 1244 */ 1245 @VisibleForTesting getBackupPayload(String key, int version)1246 public byte[] getBackupPayload(String key, int version) { 1247 if (version >= 1 && version <= 3) { 1248 Slog.wtf(TAG, "Attempting to backup UsageStats as XML with version " + version); 1249 return null; 1250 } 1251 synchronized (mLock) { 1252 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 1253 if (KEY_USAGE_STATS.equals(key)) { 1254 prune(System.currentTimeMillis()); 1255 DataOutputStream out = new DataOutputStream(baos); 1256 try { 1257 out.writeInt(version); 1258 1259 out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size()); 1260 1261 for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size(); 1262 i++) { 1263 writeIntervalStatsToStream(out, 1264 mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].valueAt(i), 1265 version); 1266 } 1267 1268 out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size()); 1269 for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size(); 1270 i++) { 1271 writeIntervalStatsToStream(out, 1272 mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].valueAt(i), 1273 version); 1274 } 1275 1276 out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size()); 1277 for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size(); 1278 i++) { 1279 writeIntervalStatsToStream(out, 1280 mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].valueAt(i), 1281 version); 1282 } 1283 1284 out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size()); 1285 for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size(); 1286 i++) { 1287 writeIntervalStatsToStream(out, 1288 mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].valueAt(i), 1289 version); 1290 } 1291 if (DEBUG) Slog.i(TAG, "Written " + baos.size() + " bytes of data"); 1292 } catch (IOException ioe) { 1293 Slog.d(TAG, "Failed to write data to output stream", ioe); 1294 baos.reset(); 1295 } 1296 } 1297 return baos.toByteArray(); 1298 } 1299 1300 } 1301 1302 /** 1303 * @hide 1304 */ 1305 @VisibleForTesting applyRestoredPayload(String key, byte[] payload)1306 public void applyRestoredPayload(String key, byte[] payload) { 1307 synchronized (mLock) { 1308 if (KEY_USAGE_STATS.equals(key)) { 1309 // Read stats files for the current device configs 1310 IntervalStats dailyConfigSource = 1311 getLatestUsageStats(UsageStatsManager.INTERVAL_DAILY); 1312 IntervalStats weeklyConfigSource = 1313 getLatestUsageStats(UsageStatsManager.INTERVAL_WEEKLY); 1314 IntervalStats monthlyConfigSource = 1315 getLatestUsageStats(UsageStatsManager.INTERVAL_MONTHLY); 1316 IntervalStats yearlyConfigSource = 1317 getLatestUsageStats(UsageStatsManager.INTERVAL_YEARLY); 1318 1319 try { 1320 DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload)); 1321 int backupDataVersion = in.readInt(); 1322 1323 // Can't handle this backup set 1324 if (backupDataVersion < 1 || backupDataVersion > BACKUP_VERSION) return; 1325 1326 // Delete all stats files 1327 // Do this after reading version and before actually restoring 1328 for (int i = 0; i < mIntervalDirs.length; i++) { 1329 deleteDirectoryContents(mIntervalDirs[i]); 1330 } 1331 1332 int fileCount = in.readInt(); 1333 for (int i = 0; i < fileCount; i++) { 1334 IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in), 1335 backupDataVersion); 1336 stats = mergeStats(stats, dailyConfigSource); 1337 putUsageStats(UsageStatsManager.INTERVAL_DAILY, stats); 1338 } 1339 1340 fileCount = in.readInt(); 1341 for (int i = 0; i < fileCount; i++) { 1342 IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in), 1343 backupDataVersion); 1344 stats = mergeStats(stats, weeklyConfigSource); 1345 putUsageStats(UsageStatsManager.INTERVAL_WEEKLY, stats); 1346 } 1347 1348 fileCount = in.readInt(); 1349 for (int i = 0; i < fileCount; i++) { 1350 IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in), 1351 backupDataVersion); 1352 stats = mergeStats(stats, monthlyConfigSource); 1353 putUsageStats(UsageStatsManager.INTERVAL_MONTHLY, stats); 1354 } 1355 1356 fileCount = in.readInt(); 1357 for (int i = 0; i < fileCount; i++) { 1358 IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in), 1359 backupDataVersion); 1360 stats = mergeStats(stats, yearlyConfigSource); 1361 putUsageStats(UsageStatsManager.INTERVAL_YEARLY, stats); 1362 } 1363 if (DEBUG) Slog.i(TAG, "Completed Restoring UsageStats"); 1364 } catch (IOException ioe) { 1365 Slog.d(TAG, "Failed to read data from input stream", ioe); 1366 } finally { 1367 indexFilesLocked(); 1368 } 1369 } 1370 } 1371 } 1372 1373 /** 1374 * Get the Configuration Statistics from the current device statistics and merge them 1375 * with the backed up usage statistics. 1376 */ mergeStats(IntervalStats beingRestored, IntervalStats onDevice)1377 private IntervalStats mergeStats(IntervalStats beingRestored, IntervalStats onDevice) { 1378 if (onDevice == null) return beingRestored; 1379 if (beingRestored == null) return null; 1380 beingRestored.activeConfiguration = onDevice.activeConfiguration; 1381 beingRestored.configurations.putAll(onDevice.configurations); 1382 beingRestored.events.clear(); 1383 beingRestored.events.merge(onDevice.events); 1384 return beingRestored; 1385 } 1386 writeIntervalStatsToStream(DataOutputStream out, AtomicFile statsFile, int version)1387 private void writeIntervalStatsToStream(DataOutputStream out, AtomicFile statsFile, int version) 1388 throws IOException { 1389 IntervalStats stats = new IntervalStats(); 1390 try { 1391 readLocked(statsFile, stats); 1392 } catch (IOException e) { 1393 Slog.e(TAG, "Failed to read usage stats file", e); 1394 out.writeInt(0); 1395 return; 1396 } 1397 sanitizeIntervalStatsForBackup(stats); 1398 byte[] data = serializeIntervalStats(stats, version); 1399 out.writeInt(data.length); 1400 out.write(data); 1401 } 1402 getIntervalStatsBytes(DataInputStream in)1403 private static byte[] getIntervalStatsBytes(DataInputStream in) throws IOException { 1404 int length = in.readInt(); 1405 byte[] buffer = new byte[length]; 1406 in.read(buffer, 0, length); 1407 return buffer; 1408 } 1409 sanitizeIntervalStatsForBackup(IntervalStats stats)1410 private static void sanitizeIntervalStatsForBackup(IntervalStats stats) { 1411 if (stats == null) return; 1412 stats.activeConfiguration = null; 1413 stats.configurations.clear(); 1414 stats.events.clear(); 1415 } 1416 serializeIntervalStats(IntervalStats stats, int version)1417 private byte[] serializeIntervalStats(IntervalStats stats, int version) { 1418 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 1419 DataOutputStream out = new DataOutputStream(baos); 1420 try { 1421 out.writeLong(stats.beginTime); 1422 writeLocked(out, stats, version, mPackagesTokenData); 1423 } catch (Exception ioe) { 1424 Slog.d(TAG, "Serializing IntervalStats Failed", ioe); 1425 baos.reset(); 1426 } 1427 return baos.toByteArray(); 1428 } 1429 deserializeIntervalStats(byte[] data, int version)1430 private IntervalStats deserializeIntervalStats(byte[] data, int version) { 1431 ByteArrayInputStream bais = new ByteArrayInputStream(data); 1432 DataInputStream in = new DataInputStream(bais); 1433 IntervalStats stats = new IntervalStats(); 1434 try { 1435 stats.beginTime = in.readLong(); 1436 readLocked(in, stats, version, mPackagesTokenData); 1437 } catch (Exception e) { 1438 Slog.d(TAG, "DeSerializing IntervalStats Failed", e); 1439 stats = null; 1440 } 1441 return stats; 1442 } 1443 deleteDirectoryContents(File directory)1444 private static void deleteDirectoryContents(File directory) { 1445 File[] files = directory.listFiles(); 1446 for (File file : files) { 1447 deleteDirectory(file); 1448 } 1449 } 1450 deleteDirectory(File directory)1451 private static void deleteDirectory(File directory) { 1452 File[] files = directory.listFiles(); 1453 if (files != null) { 1454 for (File file : files) { 1455 if (!file.isDirectory()) { 1456 file.delete(); 1457 } else { 1458 deleteDirectory(file); 1459 } 1460 } 1461 } 1462 directory.delete(); 1463 } 1464 1465 /** 1466 * Prints the obfuscated package mappings and a summary of the database files. 1467 * @param pw the print writer to print to 1468 */ dump(IndentingPrintWriter pw, boolean compact)1469 public void dump(IndentingPrintWriter pw, boolean compact) { 1470 synchronized (mLock) { 1471 pw.println(); 1472 pw.println("UsageStatsDatabase:"); 1473 pw.increaseIndent(); 1474 dumpMappings(pw); 1475 pw.decreaseIndent(); 1476 pw.println("Database Summary:"); 1477 pw.increaseIndent(); 1478 for (int i = 0; i < mSortedStatFiles.length; i++) { 1479 final TimeSparseArray<AtomicFile> files = mSortedStatFiles[i]; 1480 final int size = files.size(); 1481 pw.print(UserUsageStatsService.intervalToString(i)); 1482 pw.print(" stats files: "); 1483 pw.print(size); 1484 pw.println(", sorted list of files:"); 1485 pw.increaseIndent(); 1486 for (int f = 0; f < size; f++) { 1487 final long fileName = files.keyAt(f); 1488 if (compact) { 1489 pw.print(UserUsageStatsService.formatDateTime(fileName, false)); 1490 } else { 1491 pw.printPair(Long.toString(fileName), 1492 UserUsageStatsService.formatDateTime(fileName, true)); 1493 } 1494 pw.println(); 1495 } 1496 pw.decreaseIndent(); 1497 } 1498 pw.decreaseIndent(); 1499 } 1500 } 1501 dumpMappings(IndentingPrintWriter pw)1502 void dumpMappings(IndentingPrintWriter pw) { 1503 synchronized (mLock) { 1504 pw.println("Obfuscated Packages Mappings:"); 1505 pw.increaseIndent(); 1506 pw.println("Counter: " + mPackagesTokenData.counter); 1507 pw.println("Tokens Map Size: " + mPackagesTokenData.tokensToPackagesMap.size()); 1508 if (!mPackagesTokenData.removedPackageTokens.isEmpty()) { 1509 pw.println("Removed Package Tokens: " 1510 + Arrays.toString(mPackagesTokenData.removedPackageTokens.toArray())); 1511 } 1512 for (int i = 0; i < mPackagesTokenData.tokensToPackagesMap.size(); i++) { 1513 final int packageToken = mPackagesTokenData.tokensToPackagesMap.keyAt(i); 1514 final String packageStrings = String.join(", ", 1515 mPackagesTokenData.tokensToPackagesMap.valueAt(i)); 1516 pw.println("Token " + packageToken + ": [" + packageStrings + "]"); 1517 } 1518 pw.println(); 1519 pw.decreaseIndent(); 1520 } 1521 } 1522 readIntervalStatsForFile(int interval, long fileName)1523 IntervalStats readIntervalStatsForFile(int interval, long fileName) { 1524 synchronized (mLock) { 1525 final IntervalStats stats = new IntervalStats(); 1526 try { 1527 readLocked(mSortedStatFiles[interval].get(fileName, null), stats); 1528 return stats; 1529 } catch (Exception e) { 1530 return null; 1531 } 1532 } 1533 } 1534 } 1535