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