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