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