1 /** 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy 6 * of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations 14 * under the License. 15 */ 16 17 package com.android.server.usage; 18 19 import android.app.usage.TimeSparseArray; 20 import android.app.usage.UsageStats; 21 import android.app.usage.UsageStatsManager; 22 import android.os.Build; 23 import android.os.SystemProperties; 24 import android.util.AtomicFile; 25 import android.util.Slog; 26 import android.util.TimeUtils; 27 28 import com.android.internal.annotations.VisibleForTesting; 29 import com.android.internal.util.IndentingPrintWriter; 30 31 import libcore.io.IoUtils; 32 33 import java.io.BufferedReader; 34 import java.io.BufferedWriter; 35 import java.io.ByteArrayInputStream; 36 import java.io.ByteArrayOutputStream; 37 import java.io.DataInputStream; 38 import java.io.DataOutputStream; 39 import java.io.File; 40 import java.io.FileInputStream; 41 import java.io.FileNotFoundException; 42 import java.io.FileOutputStream; 43 import java.io.FileReader; 44 import java.io.FileWriter; 45 import java.io.FilenameFilter; 46 import java.io.IOException; 47 import java.io.InputStream; 48 import java.io.OutputStream; 49 import java.nio.file.Files; 50 import java.nio.file.StandardCopyOption; 51 import java.util.ArrayList; 52 import java.util.List; 53 54 /** 55 * Provides an interface to query for UsageStat data from a Protocol Buffer database. 56 * 57 * Prior to version 4, UsageStatsDatabase used XML to store Usage Stats data to disk. 58 * When the UsageStatsDatabase version is upgraded, the files on disk are migrated to the new 59 * version on init. The steps of migration are as follows: 60 * 1) Check if version upgrade breadcrumb exists on disk, if so skip to step 4. 61 * 2) Move current files to a timestamped backup directory. 62 * 3) Write a temporary breadcrumb file with some info about the backup directory. 63 * 4) Deserialize the backup files in the timestamped backup folder referenced by the breadcrumb. 64 * 5) Reserialize the data read from the file with the new version format and replace the old files 65 * 6) Repeat Step 3 and 4 for each file in the backup folder. 66 * 7) Update the version file with the new version and build fingerprint. 67 * 8) Delete the time stamped backup folder (unless flagged to be kept). 68 * 9) Delete the breadcrumb file. 69 * 70 * Performing the upgrade steps in this order, protects against unexpected shutdowns mid upgrade 71 * 72 * The backup directory will contain directories with timestamp names. If the upgrade breadcrumb 73 * exists on disk, it will contain a timestamp which will match one of the backup directories. The 74 * breadcrumb will also contain a version number which will denote how the files in the backup 75 * directory should be deserialized. 76 */ 77 public class UsageStatsDatabase { 78 private static final int DEFAULT_CURRENT_VERSION = 4; 79 /** 80 * Current version of the backup schema 81 * 82 * @hide 83 */ 84 @VisibleForTesting 85 public static final int BACKUP_VERSION = 4; 86 87 @VisibleForTesting 88 static final int[] MAX_FILES_PER_INTERVAL_TYPE = new int[]{100, 50, 12, 10}; 89 90 // Key under which the payload blob is stored 91 // same as UsageStatsBackupHelper.KEY_USAGE_STATS 92 static final String KEY_USAGE_STATS = "usage_stats"; 93 94 // Persist versioned backup files. 95 // Should be false, except when testing new versions 96 static final boolean KEEP_BACKUP_DIR = false; 97 98 private static final String TAG = "UsageStatsDatabase"; 99 private static final boolean DEBUG = UsageStatsService.DEBUG; 100 private static final String BAK_SUFFIX = ".bak"; 101 private static final String CHECKED_IN_SUFFIX = UsageStatsXml.CHECKED_IN_SUFFIX; 102 private static final String RETENTION_LEN_KEY = "ro.usagestats.chooser.retention"; 103 private static final int SELECTION_LOG_RETENTION_LEN = 104 SystemProperties.getInt(RETENTION_LEN_KEY, 14); 105 106 private final Object mLock = new Object(); 107 private final File[] mIntervalDirs; 108 @VisibleForTesting 109 final TimeSparseArray<AtomicFile>[] mSortedStatFiles; 110 private final UnixCalendar mCal; 111 private final File mVersionFile; 112 private final File mBackupsDir; 113 // If this file exists on disk, UsageStatsDatabase is in the middle of migrating files to a new 114 // version. If this file exists on boot, the upgrade was interrupted and needs to be picked up 115 // where it left off. 116 private final File mUpdateBreadcrumb; 117 // Current version of the database files schema 118 private int mCurrentVersion; 119 private boolean mFirstUpdate; 120 private boolean mNewUpdate; 121 122 /** 123 * UsageStatsDatabase constructor that allows setting the version number. 124 * This should only be used for testing. 125 * 126 * @hide 127 */ 128 @VisibleForTesting UsageStatsDatabase(File dir, int version)129 public UsageStatsDatabase(File dir, int version) { 130 mIntervalDirs = new File[]{ 131 new File(dir, "daily"), 132 new File(dir, "weekly"), 133 new File(dir, "monthly"), 134 new File(dir, "yearly"), 135 }; 136 mCurrentVersion = version; 137 mVersionFile = new File(dir, "version"); 138 mBackupsDir = new File(dir, "backups"); 139 mUpdateBreadcrumb = new File(dir, "breadcrumb"); 140 mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length]; 141 mCal = new UnixCalendar(0); 142 } 143 UsageStatsDatabase(File dir)144 public UsageStatsDatabase(File dir) { 145 this(dir, DEFAULT_CURRENT_VERSION); 146 } 147 148 /** 149 * Initialize any directories required and index what stats are available. 150 */ init(long currentTimeMillis)151 public void init(long currentTimeMillis) { 152 synchronized (mLock) { 153 for (File f : mIntervalDirs) { 154 f.mkdirs(); 155 if (!f.exists()) { 156 throw new IllegalStateException("Failed to create directory " 157 + f.getAbsolutePath()); 158 } 159 } 160 161 checkVersionAndBuildLocked(); 162 indexFilesLocked(); 163 164 // Delete files that are in the future. 165 for (TimeSparseArray<AtomicFile> files : mSortedStatFiles) { 166 final int startIndex = files.closestIndexOnOrAfter(currentTimeMillis); 167 if (startIndex < 0) { 168 continue; 169 } 170 171 final int fileCount = files.size(); 172 for (int i = startIndex; i < fileCount; i++) { 173 files.valueAt(i).delete(); 174 } 175 176 // Remove in a separate loop because any accesses (valueAt) 177 // will cause a gc in the SparseArray and mess up the order. 178 for (int i = startIndex; i < fileCount; i++) { 179 files.removeAt(i); 180 } 181 } 182 } 183 } 184 185 public interface CheckinAction { checkin(IntervalStats stats)186 boolean checkin(IntervalStats stats); 187 } 188 189 /** 190 * Calls {@link CheckinAction#checkin(IntervalStats)} on the given {@link CheckinAction} 191 * for all {@link IntervalStats} that haven't been checked-in. 192 * If any of the calls to {@link CheckinAction#checkin(IntervalStats)} returns false or throws 193 * an exception, the check-in will be aborted. 194 * 195 * @param checkinAction The callback to run when checking-in {@link IntervalStats}. 196 * @return true if the check-in succeeded. 197 */ checkinDailyFiles(CheckinAction checkinAction)198 public boolean checkinDailyFiles(CheckinAction checkinAction) { 199 synchronized (mLock) { 200 final TimeSparseArray<AtomicFile> files = 201 mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY]; 202 final int fileCount = files.size(); 203 204 // We may have holes in the checkin (if there was an error) 205 // so find the last checked-in file and go from there. 206 int lastCheckin = -1; 207 for (int i = 0; i < fileCount - 1; i++) { 208 if (files.valueAt(i).getBaseFile().getPath().endsWith(CHECKED_IN_SUFFIX)) { 209 lastCheckin = i; 210 } 211 } 212 213 final int start = lastCheckin + 1; 214 if (start == fileCount - 1) { 215 return true; 216 } 217 218 try { 219 IntervalStats stats = new IntervalStats(); 220 for (int i = start; i < fileCount - 1; i++) { 221 readLocked(files.valueAt(i), stats); 222 if (!checkinAction.checkin(stats)) { 223 return false; 224 } 225 } 226 } catch (IOException e) { 227 Slog.e(TAG, "Failed to check-in", e); 228 return false; 229 } 230 231 // We have successfully checked-in the stats, so rename the files so that they 232 // are marked as checked-in. 233 for (int i = start; i < fileCount - 1; i++) { 234 final AtomicFile file = files.valueAt(i); 235 final File checkedInFile = new File( 236 file.getBaseFile().getPath() + CHECKED_IN_SUFFIX); 237 if (!file.getBaseFile().renameTo(checkedInFile)) { 238 // We must return success, as we've already marked some files as checked-in. 239 // It's better to repeat ourselves than to lose data. 240 Slog.e(TAG, "Failed to mark file " + file.getBaseFile().getPath() 241 + " as checked-in"); 242 return true; 243 } 244 245 // AtomicFile needs to set a new backup path with the same -c extension, so 246 // we replace the old AtomicFile with the updated one. 247 files.setValueAt(i, new AtomicFile(checkedInFile)); 248 } 249 } 250 return true; 251 } 252 253 /** @hide */ 254 @VisibleForTesting forceIndexFiles()255 void forceIndexFiles() { 256 synchronized (mLock) { 257 indexFilesLocked(); 258 } 259 } 260 indexFilesLocked()261 private void indexFilesLocked() { 262 final FilenameFilter backupFileFilter = new FilenameFilter() { 263 @Override 264 public boolean accept(File dir, String name) { 265 return !name.endsWith(BAK_SUFFIX); 266 } 267 }; 268 // Index the available usage stat files on disk. 269 for (int i = 0; i < mSortedStatFiles.length; i++) { 270 if (mSortedStatFiles[i] == null) { 271 mSortedStatFiles[i] = new TimeSparseArray<>(); 272 } else { 273 mSortedStatFiles[i].clear(); 274 } 275 File[] files = mIntervalDirs[i].listFiles(backupFileFilter); 276 if (files != null) { 277 if (DEBUG) { 278 Slog.d(TAG, "Found " + files.length + " stat files for interval " + i); 279 } 280 final int len = files.length; 281 for (int j = 0; j < len; j++) { 282 final File f = files[j]; 283 final AtomicFile af = new AtomicFile(f); 284 try { 285 mSortedStatFiles[i].put(parseBeginTime(af), af); 286 } catch (IOException e) { 287 Slog.e(TAG, "failed to index file: " + f, e); 288 } 289 } 290 291 // only keep the max allowed number of files for each interval type. 292 final int toDelete = mSortedStatFiles[i].size() - MAX_FILES_PER_INTERVAL_TYPE[i]; 293 if (toDelete > 0) { 294 for (int j = 0; j < toDelete; j++) { 295 mSortedStatFiles[i].valueAt(0).delete(); 296 mSortedStatFiles[i].removeAt(0); 297 } 298 Slog.d(TAG, "Deleted " + toDelete + " stat files for interval " + i); 299 } 300 } 301 } 302 } 303 304 /** 305 * Is this the first update to the system from L to M? 306 */ isFirstUpdate()307 boolean isFirstUpdate() { 308 return mFirstUpdate; 309 } 310 311 /** 312 * Is this a system update since we started tracking build fingerprint in the version file? 313 */ isNewUpdate()314 boolean isNewUpdate() { 315 return mNewUpdate; 316 } 317 checkVersionAndBuildLocked()318 private void checkVersionAndBuildLocked() { 319 int version; 320 String buildFingerprint; 321 String currentFingerprint = getBuildFingerprint(); 322 mFirstUpdate = true; 323 mNewUpdate = true; 324 try (BufferedReader reader = new BufferedReader(new FileReader(mVersionFile))) { 325 version = Integer.parseInt(reader.readLine()); 326 buildFingerprint = reader.readLine(); 327 if (buildFingerprint != null) { 328 mFirstUpdate = false; 329 } 330 if (currentFingerprint.equals(buildFingerprint)) { 331 mNewUpdate = false; 332 } 333 } catch (NumberFormatException | IOException e) { 334 version = 0; 335 } 336 337 if (version != mCurrentVersion) { 338 Slog.i(TAG, "Upgrading from version " + version + " to " + mCurrentVersion); 339 if (!mUpdateBreadcrumb.exists()) { 340 try { 341 doUpgradeLocked(version); 342 } catch (Exception e) { 343 Slog.e(TAG, 344 "Failed to upgrade from version " + version + " to " + mCurrentVersion, 345 e); 346 // Fallback to previous version. 347 mCurrentVersion = version; 348 return; 349 } 350 } else { 351 Slog.i(TAG, "Version upgrade breadcrumb found on disk! Continuing version upgrade"); 352 } 353 } 354 355 if (mUpdateBreadcrumb.exists()) { 356 int previousVersion; 357 long token; 358 try (BufferedReader reader = new BufferedReader( 359 new FileReader(mUpdateBreadcrumb))) { 360 token = Long.parseLong(reader.readLine()); 361 previousVersion = Integer.parseInt(reader.readLine()); 362 } catch (NumberFormatException | IOException e) { 363 Slog.e(TAG, "Failed read version upgrade breadcrumb"); 364 throw new RuntimeException(e); 365 } 366 continueUpgradeLocked(previousVersion, token); 367 } 368 369 if (version != mCurrentVersion || mNewUpdate) { 370 try (BufferedWriter writer = new BufferedWriter(new FileWriter(mVersionFile))) { 371 writer.write(Integer.toString(mCurrentVersion)); 372 writer.write("\n"); 373 writer.write(currentFingerprint); 374 writer.write("\n"); 375 writer.flush(); 376 } catch (IOException e) { 377 Slog.e(TAG, "Failed to write new version"); 378 throw new RuntimeException(e); 379 } 380 } 381 382 if (mUpdateBreadcrumb.exists()) { 383 // Files should be up to date with current version. Clear the version update breadcrumb 384 mUpdateBreadcrumb.delete(); 385 } 386 387 if (mBackupsDir.exists() && !KEEP_BACKUP_DIR) { 388 deleteDirectory(mBackupsDir); 389 } 390 } 391 getBuildFingerprint()392 private String getBuildFingerprint() { 393 return Build.VERSION.RELEASE + ";" 394 + Build.VERSION.CODENAME + ";" 395 + Build.VERSION.INCREMENTAL; 396 } 397 doUpgradeLocked(int thisVersion)398 private void doUpgradeLocked(int thisVersion) { 399 if (thisVersion < 2) { 400 // Delete all files if we are version 0. This is a pre-release version, 401 // so this is fine. 402 Slog.i(TAG, "Deleting all usage stats files"); 403 for (int i = 0; i < mIntervalDirs.length; i++) { 404 File[] files = mIntervalDirs[i].listFiles(); 405 if (files != null) { 406 for (File f : files) { 407 f.delete(); 408 } 409 } 410 } 411 } else { 412 // Create a dir in backups based on current timestamp 413 final long token = System.currentTimeMillis(); 414 final File backupDir = new File(mBackupsDir, Long.toString(token)); 415 backupDir.mkdirs(); 416 if (!backupDir.exists()) { 417 throw new IllegalStateException( 418 "Failed to create backup directory " + backupDir.getAbsolutePath()); 419 } 420 try { 421 Files.copy(mVersionFile.toPath(), 422 new File(backupDir, mVersionFile.getName()).toPath(), 423 StandardCopyOption.REPLACE_EXISTING); 424 } catch (IOException e) { 425 Slog.e(TAG, "Failed to back up version file : " + mVersionFile.toString()); 426 throw new RuntimeException(e); 427 } 428 429 for (int i = 0; i < mIntervalDirs.length; i++) { 430 final File backupIntervalDir = new File(backupDir, mIntervalDirs[i].getName()); 431 backupIntervalDir.mkdir(); 432 433 if (!backupIntervalDir.exists()) { 434 throw new IllegalStateException( 435 "Failed to create interval backup directory " 436 + backupIntervalDir.getAbsolutePath()); 437 } 438 File[] files = mIntervalDirs[i].listFiles(); 439 if (files != null) { 440 for (int j = 0; j < files.length; j++) { 441 final File backupFile = new File(backupIntervalDir, files[j].getName()); 442 if (DEBUG) { 443 Slog.d(TAG, "Creating versioned (" + Integer.toString(thisVersion) 444 + ") backup of " + files[j].toString() 445 + " stat files for interval " 446 + i + " to " + backupFile.toString()); 447 } 448 449 try { 450 // Backup file should not already exist, but make sure it doesn't 451 Files.move(files[j].toPath(), backupFile.toPath(), 452 StandardCopyOption.REPLACE_EXISTING); 453 } catch (IOException e) { 454 Slog.e(TAG, "Failed to back up file : " + files[j].toString()); 455 throw new RuntimeException(e); 456 } 457 } 458 } 459 } 460 461 // Leave a breadcrumb behind noting that all the usage stats have been moved to a backup 462 BufferedWriter writer = null; 463 try { 464 writer = new BufferedWriter(new FileWriter(mUpdateBreadcrumb)); 465 writer.write(Long.toString(token)); 466 writer.write("\n"); 467 writer.write(Integer.toString(thisVersion)); 468 writer.write("\n"); 469 writer.flush(); 470 } catch (IOException e) { 471 Slog.e(TAG, "Failed to write new version upgrade breadcrumb"); 472 throw new RuntimeException(e); 473 } finally { 474 IoUtils.closeQuietly(writer); 475 } 476 } 477 } 478 continueUpgradeLocked(int version, long token)479 private void continueUpgradeLocked(int version, long token) { 480 final File backupDir = new File(mBackupsDir, Long.toString(token)); 481 482 // Read each file in the backup according to the version and write to the interval 483 // directories in the current versions format 484 for (int i = 0; i < mIntervalDirs.length; i++) { 485 final File backedUpInterval = new File(backupDir, mIntervalDirs[i].getName()); 486 File[] files = backedUpInterval.listFiles(); 487 if (files != null) { 488 for (int j = 0; j < files.length; j++) { 489 if (DEBUG) { 490 Slog.d(TAG, 491 "Upgrading " + files[j].toString() + " to version (" 492 + Integer.toString( 493 mCurrentVersion) + ") for interval " + i); 494 } 495 try { 496 IntervalStats stats = new IntervalStats(); 497 readLocked(new AtomicFile(files[j]), stats, version); 498 writeLocked(new AtomicFile(new File(mIntervalDirs[i], 499 Long.toString(stats.beginTime))), stats, mCurrentVersion); 500 } catch (Exception e) { 501 // This method is called on boot, log the exception and move on 502 Slog.e(TAG, "Failed to upgrade backup file : " + files[j].toString()); 503 } 504 } 505 } 506 } 507 } 508 onTimeChanged(long timeDiffMillis)509 public void onTimeChanged(long timeDiffMillis) { 510 synchronized (mLock) { 511 StringBuilder logBuilder = new StringBuilder(); 512 logBuilder.append("Time changed by "); 513 TimeUtils.formatDuration(timeDiffMillis, logBuilder); 514 logBuilder.append("."); 515 516 int filesDeleted = 0; 517 int filesMoved = 0; 518 519 for (TimeSparseArray<AtomicFile> files : mSortedStatFiles) { 520 final int fileCount = files.size(); 521 for (int i = 0; i < fileCount; i++) { 522 final AtomicFile file = files.valueAt(i); 523 final long newTime = files.keyAt(i) + timeDiffMillis; 524 if (newTime < 0) { 525 filesDeleted++; 526 file.delete(); 527 } else { 528 try { 529 file.openRead().close(); 530 } catch (IOException e) { 531 // Ignore, this is just to make sure there are no backups. 532 } 533 534 String newName = Long.toString(newTime); 535 if (file.getBaseFile().getName().endsWith(CHECKED_IN_SUFFIX)) { 536 newName = newName + CHECKED_IN_SUFFIX; 537 } 538 539 final File newFile = new File(file.getBaseFile().getParentFile(), newName); 540 filesMoved++; 541 file.getBaseFile().renameTo(newFile); 542 } 543 } 544 files.clear(); 545 } 546 547 logBuilder.append(" files deleted: ").append(filesDeleted); 548 logBuilder.append(" files moved: ").append(filesMoved); 549 Slog.i(TAG, logBuilder.toString()); 550 551 // Now re-index the new files. 552 indexFilesLocked(); 553 } 554 } 555 556 /** 557 * Get the latest stats that exist for this interval type. 558 */ getLatestUsageStats(int intervalType)559 public IntervalStats getLatestUsageStats(int intervalType) { 560 synchronized (mLock) { 561 if (intervalType < 0 || intervalType >= mIntervalDirs.length) { 562 throw new IllegalArgumentException("Bad interval type " + intervalType); 563 } 564 565 final int fileCount = mSortedStatFiles[intervalType].size(); 566 if (fileCount == 0) { 567 return null; 568 } 569 570 try { 571 final AtomicFile f = mSortedStatFiles[intervalType].valueAt(fileCount - 1); 572 IntervalStats stats = new IntervalStats(); 573 readLocked(f, stats); 574 return stats; 575 } catch (IOException e) { 576 Slog.e(TAG, "Failed to read usage stats file", e); 577 } 578 } 579 return null; 580 } 581 582 /** 583 * Figures out what to extract from the given IntervalStats object. 584 */ 585 public interface StatCombiner<T> { 586 587 /** 588 * Implementations should extract interesting from <code>stats</code> and add it 589 * to the <code>accumulatedResult</code> list. 590 * 591 * If the <code>stats</code> object is mutable, <code>mutable</code> will be true, 592 * which means you should make a copy of the data before adding it to the 593 * <code>accumulatedResult</code> list. 594 * 595 * @param stats The {@link IntervalStats} object selected. 596 * @param mutable Whether or not the data inside the stats object is mutable. 597 * @param accumulatedResult The list to which to add extracted data. 598 */ combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult)599 void combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult); 600 } 601 602 /** 603 * Find all {@link IntervalStats} for the given range and interval type. 604 */ queryUsageStats(int intervalType, long beginTime, long endTime, StatCombiner<T> combiner)605 public <T> List<T> queryUsageStats(int intervalType, long beginTime, long endTime, 606 StatCombiner<T> combiner) { 607 synchronized (mLock) { 608 if (intervalType < 0 || intervalType >= mIntervalDirs.length) { 609 throw new IllegalArgumentException("Bad interval type " + intervalType); 610 } 611 612 final TimeSparseArray<AtomicFile> intervalStats = mSortedStatFiles[intervalType]; 613 614 if (endTime <= beginTime) { 615 if (DEBUG) { 616 Slog.d(TAG, "endTime(" + endTime + ") <= beginTime(" + beginTime + ")"); 617 } 618 return null; 619 } 620 621 int startIndex = intervalStats.closestIndexOnOrBefore(beginTime); 622 if (startIndex < 0) { 623 // All the stats available have timestamps after beginTime, which means they all 624 // match. 625 startIndex = 0; 626 } 627 628 int endIndex = intervalStats.closestIndexOnOrBefore(endTime); 629 if (endIndex < 0) { 630 // All the stats start after this range ends, so nothing matches. 631 if (DEBUG) { 632 Slog.d(TAG, "No results for this range. All stats start after."); 633 } 634 return null; 635 } 636 637 if (intervalStats.keyAt(endIndex) == endTime) { 638 // The endTime is exclusive, so if we matched exactly take the one before. 639 endIndex--; 640 if (endIndex < 0) { 641 // All the stats start after this range ends, so nothing matches. 642 if (DEBUG) { 643 Slog.d(TAG, "No results for this range. All stats start after."); 644 } 645 return null; 646 } 647 } 648 649 final IntervalStats stats = new IntervalStats(); 650 final ArrayList<T> results = new ArrayList<>(); 651 for (int i = startIndex; i <= endIndex; i++) { 652 final AtomicFile f = intervalStats.valueAt(i); 653 654 if (DEBUG) { 655 Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath()); 656 } 657 658 try { 659 readLocked(f, stats); 660 if (beginTime < stats.endTime) { 661 combiner.combine(stats, false, results); 662 } 663 } catch (IOException e) { 664 Slog.e(TAG, "Failed to read usage stats file", e); 665 // We continue so that we return results that are not 666 // corrupt. 667 } 668 } 669 return results; 670 } 671 } 672 673 /** 674 * Find the interval that best matches this range. 675 * 676 * TODO(adamlesinski): Use endTimeStamp in best fit calculation. 677 */ findBestFitBucket(long beginTimeStamp, long endTimeStamp)678 public int findBestFitBucket(long beginTimeStamp, long endTimeStamp) { 679 synchronized (mLock) { 680 int bestBucket = -1; 681 long smallestDiff = Long.MAX_VALUE; 682 for (int i = mSortedStatFiles.length - 1; i >= 0; i--) { 683 final int index = mSortedStatFiles[i].closestIndexOnOrBefore(beginTimeStamp); 684 int size = mSortedStatFiles[i].size(); 685 if (index >= 0 && index < size) { 686 // We have some results here, check if they are better than our current match. 687 long diff = Math.abs(mSortedStatFiles[i].keyAt(index) - beginTimeStamp); 688 if (diff < smallestDiff) { 689 smallestDiff = diff; 690 bestBucket = i; 691 } 692 } 693 } 694 return bestBucket; 695 } 696 } 697 698 /** 699 * Remove any usage stat files that are too old. 700 */ prune(final long currentTimeMillis)701 public void prune(final long currentTimeMillis) { 702 synchronized (mLock) { 703 mCal.setTimeInMillis(currentTimeMillis); 704 mCal.addYears(-3); 705 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_YEARLY], 706 mCal.getTimeInMillis()); 707 708 mCal.setTimeInMillis(currentTimeMillis); 709 mCal.addMonths(-6); 710 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_MONTHLY], 711 mCal.getTimeInMillis()); 712 713 mCal.setTimeInMillis(currentTimeMillis); 714 mCal.addWeeks(-4); 715 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_WEEKLY], 716 mCal.getTimeInMillis()); 717 718 mCal.setTimeInMillis(currentTimeMillis); 719 mCal.addDays(-10); 720 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_DAILY], 721 mCal.getTimeInMillis()); 722 723 mCal.setTimeInMillis(currentTimeMillis); 724 mCal.addDays(-SELECTION_LOG_RETENTION_LEN); 725 for (int i = 0; i < mIntervalDirs.length; ++i) { 726 pruneChooserCountsOlderThan(mIntervalDirs[i], mCal.getTimeInMillis()); 727 } 728 729 // We must re-index our file list or we will be trying to read 730 // deleted files. 731 indexFilesLocked(); 732 } 733 } 734 pruneFilesOlderThan(File dir, long expiryTime)735 private static void pruneFilesOlderThan(File dir, long expiryTime) { 736 File[] files = dir.listFiles(); 737 if (files != null) { 738 for (File f : files) { 739 long beginTime; 740 try { 741 beginTime = parseBeginTime(f); 742 } catch (IOException e) { 743 beginTime = 0; 744 } 745 746 if (beginTime < expiryTime) { 747 new AtomicFile(f).delete(); 748 } 749 } 750 } 751 } 752 pruneChooserCountsOlderThan(File dir, long expiryTime)753 private void pruneChooserCountsOlderThan(File dir, long expiryTime) { 754 File[] files = dir.listFiles(); 755 if (files != null) { 756 for (File f : files) { 757 long beginTime; 758 try { 759 beginTime = parseBeginTime(f); 760 } catch (IOException e) { 761 beginTime = 0; 762 } 763 764 if (beginTime < expiryTime) { 765 try { 766 final AtomicFile af = new AtomicFile(f); 767 final IntervalStats stats = new IntervalStats(); 768 readLocked(af, stats); 769 final int pkgCount = stats.packageStats.size(); 770 for (int i = 0; i < pkgCount; i++) { 771 UsageStats pkgStats = stats.packageStats.valueAt(i); 772 if (pkgStats.mChooserCounts != null) { 773 pkgStats.mChooserCounts.clear(); 774 } 775 } 776 writeLocked(af, stats); 777 } catch (Exception e) { 778 Slog.e(TAG, "Failed to delete chooser counts from usage stats file", e); 779 } 780 } 781 } 782 } 783 } 784 785 parseBeginTime(AtomicFile file)786 private static long parseBeginTime(AtomicFile file) throws IOException { 787 return parseBeginTime(file.getBaseFile()); 788 } 789 parseBeginTime(File file)790 private static long parseBeginTime(File file) throws IOException { 791 String name = file.getName(); 792 793 // Parse out the digits from the the front of the file name 794 for (int i = 0; i < name.length(); i++) { 795 final char c = name.charAt(i); 796 if (c < '0' || c > '9') { 797 // found first char that is not a digit. 798 name = name.substring(0, i); 799 break; 800 } 801 } 802 803 try { 804 return Long.parseLong(name); 805 } catch (NumberFormatException e) { 806 throw new IOException(e); 807 } 808 } 809 writeLocked(AtomicFile file, IntervalStats stats)810 private void writeLocked(AtomicFile file, IntervalStats stats) throws IOException { 811 writeLocked(file, stats, mCurrentVersion); 812 } 813 writeLocked(AtomicFile file, IntervalStats stats, int version)814 private static void writeLocked(AtomicFile file, IntervalStats stats, int version) 815 throws IOException { 816 FileOutputStream fos = file.startWrite(); 817 try { 818 writeLocked(fos, stats, version); 819 file.finishWrite(fos); 820 fos = null; 821 } finally { 822 // When fos is null (successful write), this will no-op 823 file.failWrite(fos); 824 } 825 } 826 writeLocked(OutputStream out, IntervalStats stats)827 private void writeLocked(OutputStream out, IntervalStats stats) throws IOException { 828 writeLocked(out, stats, mCurrentVersion); 829 } 830 writeLocked(OutputStream out, IntervalStats stats, int version)831 private static void writeLocked(OutputStream out, IntervalStats stats, int version) 832 throws IOException { 833 switch (version) { 834 case 1: 835 case 2: 836 case 3: 837 UsageStatsXml.write(out, stats); 838 break; 839 case 4: 840 UsageStatsProto.write(out, stats); 841 break; 842 default: 843 throw new RuntimeException( 844 "Unhandled UsageStatsDatabase version: " + Integer.toString(version) 845 + " on write."); 846 } 847 } 848 readLocked(AtomicFile file, IntervalStats statsOut)849 private void readLocked(AtomicFile file, IntervalStats statsOut) throws IOException { 850 readLocked(file, statsOut, mCurrentVersion); 851 } 852 readLocked(AtomicFile file, IntervalStats statsOut, int version)853 private static void readLocked(AtomicFile file, IntervalStats statsOut, int version) 854 throws IOException { 855 try { 856 FileInputStream in = file.openRead(); 857 try { 858 statsOut.beginTime = parseBeginTime(file); 859 readLocked(in, statsOut, version); 860 statsOut.lastTimeSaved = file.getLastModifiedTime(); 861 } finally { 862 try { 863 in.close(); 864 } catch (IOException e) { 865 // Empty 866 } 867 } 868 } catch (FileNotFoundException e) { 869 Slog.e(TAG, "UsageStatsDatabase", e); 870 throw e; 871 } 872 } 873 readLocked(InputStream in, IntervalStats statsOut)874 private void readLocked(InputStream in, IntervalStats statsOut) throws IOException { 875 readLocked(in, statsOut, mCurrentVersion); 876 } 877 readLocked(InputStream in, IntervalStats statsOut, int version)878 private static void readLocked(InputStream in, IntervalStats statsOut, int version) 879 throws IOException { 880 switch (version) { 881 case 1: 882 case 2: 883 case 3: 884 UsageStatsXml.read(in, statsOut); 885 break; 886 case 4: 887 UsageStatsProto.read(in, statsOut); 888 break; 889 default: 890 throw new RuntimeException( 891 "Unhandled UsageStatsDatabase version: " + Integer.toString(version) 892 + " on read."); 893 } 894 895 } 896 897 /** 898 * Update the stats in the database. They may not be written to disk immediately. 899 */ putUsageStats(int intervalType, IntervalStats stats)900 public void putUsageStats(int intervalType, IntervalStats stats) throws IOException { 901 if (stats == null) return; 902 synchronized (mLock) { 903 if (intervalType < 0 || intervalType >= mIntervalDirs.length) { 904 throw new IllegalArgumentException("Bad interval type " + intervalType); 905 } 906 907 AtomicFile f = mSortedStatFiles[intervalType].get(stats.beginTime); 908 if (f == null) { 909 f = new AtomicFile(new File(mIntervalDirs[intervalType], 910 Long.toString(stats.beginTime))); 911 mSortedStatFiles[intervalType].put(stats.beginTime, f); 912 } 913 914 writeLocked(f, stats); 915 stats.lastTimeSaved = f.getLastModifiedTime(); 916 } 917 } 918 919 920 /* Backup/Restore Code */ getBackupPayload(String key)921 byte[] getBackupPayload(String key) { 922 return getBackupPayload(key, BACKUP_VERSION); 923 } 924 925 /** 926 * @hide 927 */ 928 @VisibleForTesting getBackupPayload(String key, int version)929 public byte[] getBackupPayload(String key, int version) { 930 synchronized (mLock) { 931 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 932 if (KEY_USAGE_STATS.equals(key)) { 933 prune(System.currentTimeMillis()); 934 DataOutputStream out = new DataOutputStream(baos); 935 try { 936 out.writeInt(version); 937 938 out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size()); 939 940 for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size(); 941 i++) { 942 writeIntervalStatsToStream(out, 943 mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].valueAt(i), 944 version); 945 } 946 947 out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size()); 948 for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size(); 949 i++) { 950 writeIntervalStatsToStream(out, 951 mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].valueAt(i), 952 version); 953 } 954 955 out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size()); 956 for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size(); 957 i++) { 958 writeIntervalStatsToStream(out, 959 mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].valueAt(i), 960 version); 961 } 962 963 out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size()); 964 for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size(); 965 i++) { 966 writeIntervalStatsToStream(out, 967 mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].valueAt(i), 968 version); 969 } 970 if (DEBUG) Slog.i(TAG, "Written " + baos.size() + " bytes of data"); 971 } catch (IOException ioe) { 972 Slog.d(TAG, "Failed to write data to output stream", ioe); 973 baos.reset(); 974 } 975 } 976 return baos.toByteArray(); 977 } 978 979 } 980 981 /** 982 * @hide 983 */ 984 @VisibleForTesting applyRestoredPayload(String key, byte[] payload)985 public void applyRestoredPayload(String key, byte[] payload) { 986 synchronized (mLock) { 987 if (KEY_USAGE_STATS.equals(key)) { 988 // Read stats files for the current device configs 989 IntervalStats dailyConfigSource = 990 getLatestUsageStats(UsageStatsManager.INTERVAL_DAILY); 991 IntervalStats weeklyConfigSource = 992 getLatestUsageStats(UsageStatsManager.INTERVAL_WEEKLY); 993 IntervalStats monthlyConfigSource = 994 getLatestUsageStats(UsageStatsManager.INTERVAL_MONTHLY); 995 IntervalStats yearlyConfigSource = 996 getLatestUsageStats(UsageStatsManager.INTERVAL_YEARLY); 997 998 try { 999 DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload)); 1000 int backupDataVersion = in.readInt(); 1001 1002 // Can't handle this backup set 1003 if (backupDataVersion < 1 || backupDataVersion > BACKUP_VERSION) return; 1004 1005 // Delete all stats files 1006 // Do this after reading version and before actually restoring 1007 for (int i = 0; i < mIntervalDirs.length; i++) { 1008 deleteDirectoryContents(mIntervalDirs[i]); 1009 } 1010 1011 int fileCount = in.readInt(); 1012 for (int i = 0; i < fileCount; i++) { 1013 IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in), 1014 backupDataVersion); 1015 stats = mergeStats(stats, dailyConfigSource); 1016 putUsageStats(UsageStatsManager.INTERVAL_DAILY, stats); 1017 } 1018 1019 fileCount = in.readInt(); 1020 for (int i = 0; i < fileCount; i++) { 1021 IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in), 1022 backupDataVersion); 1023 stats = mergeStats(stats, weeklyConfigSource); 1024 putUsageStats(UsageStatsManager.INTERVAL_WEEKLY, stats); 1025 } 1026 1027 fileCount = in.readInt(); 1028 for (int i = 0; i < fileCount; i++) { 1029 IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in), 1030 backupDataVersion); 1031 stats = mergeStats(stats, monthlyConfigSource); 1032 putUsageStats(UsageStatsManager.INTERVAL_MONTHLY, stats); 1033 } 1034 1035 fileCount = in.readInt(); 1036 for (int i = 0; i < fileCount; i++) { 1037 IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in), 1038 backupDataVersion); 1039 stats = mergeStats(stats, yearlyConfigSource); 1040 putUsageStats(UsageStatsManager.INTERVAL_YEARLY, stats); 1041 } 1042 if (DEBUG) Slog.i(TAG, "Completed Restoring UsageStats"); 1043 } catch (IOException ioe) { 1044 Slog.d(TAG, "Failed to read data from input stream", ioe); 1045 } finally { 1046 indexFilesLocked(); 1047 } 1048 } 1049 } 1050 } 1051 1052 /** 1053 * Get the Configuration Statistics from the current device statistics and merge them 1054 * with the backed up usage statistics. 1055 */ mergeStats(IntervalStats beingRestored, IntervalStats onDevice)1056 private IntervalStats mergeStats(IntervalStats beingRestored, IntervalStats onDevice) { 1057 if (onDevice == null) return beingRestored; 1058 if (beingRestored == null) return null; 1059 beingRestored.activeConfiguration = onDevice.activeConfiguration; 1060 beingRestored.configurations.putAll(onDevice.configurations); 1061 beingRestored.events.clear(); 1062 beingRestored.events.merge(onDevice.events); 1063 return beingRestored; 1064 } 1065 writeIntervalStatsToStream(DataOutputStream out, AtomicFile statsFile, int version)1066 private void writeIntervalStatsToStream(DataOutputStream out, AtomicFile statsFile, int version) 1067 throws IOException { 1068 IntervalStats stats = new IntervalStats(); 1069 try { 1070 readLocked(statsFile, stats); 1071 } catch (IOException e) { 1072 Slog.e(TAG, "Failed to read usage stats file", e); 1073 out.writeInt(0); 1074 return; 1075 } 1076 sanitizeIntervalStatsForBackup(stats); 1077 byte[] data = serializeIntervalStats(stats, version); 1078 out.writeInt(data.length); 1079 out.write(data); 1080 } 1081 getIntervalStatsBytes(DataInputStream in)1082 private static byte[] getIntervalStatsBytes(DataInputStream in) throws IOException { 1083 int length = in.readInt(); 1084 byte[] buffer = new byte[length]; 1085 in.read(buffer, 0, length); 1086 return buffer; 1087 } 1088 sanitizeIntervalStatsForBackup(IntervalStats stats)1089 private static void sanitizeIntervalStatsForBackup(IntervalStats stats) { 1090 if (stats == null) return; 1091 stats.activeConfiguration = null; 1092 stats.configurations.clear(); 1093 stats.events.clear(); 1094 } 1095 serializeIntervalStats(IntervalStats stats, int version)1096 private byte[] serializeIntervalStats(IntervalStats stats, int version) { 1097 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 1098 DataOutputStream out = new DataOutputStream(baos); 1099 try { 1100 out.writeLong(stats.beginTime); 1101 writeLocked(out, stats, version); 1102 } catch (Exception ioe) { 1103 Slog.d(TAG, "Serializing IntervalStats Failed", ioe); 1104 baos.reset(); 1105 } 1106 return baos.toByteArray(); 1107 } 1108 deserializeIntervalStats(byte[] data, int version)1109 private IntervalStats deserializeIntervalStats(byte[] data, int version) { 1110 ByteArrayInputStream bais = new ByteArrayInputStream(data); 1111 DataInputStream in = new DataInputStream(bais); 1112 IntervalStats stats = new IntervalStats(); 1113 try { 1114 stats.beginTime = in.readLong(); 1115 readLocked(in, stats, version); 1116 } catch (IOException ioe) { 1117 Slog.d(TAG, "DeSerializing IntervalStats Failed", ioe); 1118 stats = null; 1119 } 1120 return stats; 1121 } 1122 deleteDirectoryContents(File directory)1123 private static void deleteDirectoryContents(File directory) { 1124 File[] files = directory.listFiles(); 1125 for (File file : files) { 1126 deleteDirectory(file); 1127 } 1128 } 1129 deleteDirectory(File directory)1130 private static void deleteDirectory(File directory) { 1131 File[] files = directory.listFiles(); 1132 if (files != null) { 1133 for (File file : files) { 1134 if (!file.isDirectory()) { 1135 file.delete(); 1136 } else { 1137 deleteDirectory(file); 1138 } 1139 } 1140 } 1141 directory.delete(); 1142 } 1143 1144 /** 1145 * print total number and list of stats files for each interval type. 1146 * @param pw 1147 */ dump(IndentingPrintWriter pw, boolean compact)1148 public void dump(IndentingPrintWriter pw, boolean compact) { 1149 synchronized (mLock) { 1150 pw.println("UsageStatsDatabase:"); 1151 pw.increaseIndent(); 1152 for (int i = 0; i < mSortedStatFiles.length; i++) { 1153 final TimeSparseArray<AtomicFile> files = mSortedStatFiles[i]; 1154 final int size = files.size(); 1155 pw.print(UserUsageStatsService.intervalToString(i)); 1156 pw.print(" stats files: "); 1157 pw.print(size); 1158 pw.println(", sorted list of files:"); 1159 pw.increaseIndent(); 1160 for (int f = 0; f < size; f++) { 1161 final long fileName = files.keyAt(f); 1162 if (compact) { 1163 pw.print(UserUsageStatsService.formatDateTime(fileName, false)); 1164 } else { 1165 pw.printPair(Long.toString(fileName), 1166 UserUsageStatsService.formatDateTime(fileName, true)); 1167 } 1168 pw.println(); 1169 } 1170 pw.decreaseIndent(); 1171 } 1172 pw.decreaseIndent(); 1173 } 1174 } 1175 readIntervalStatsForFile(int interval, long fileName)1176 IntervalStats readIntervalStatsForFile(int interval, long fileName) { 1177 synchronized (mLock) { 1178 final IntervalStats stats = new IntervalStats(); 1179 try { 1180 readLocked(mSortedStatFiles[interval].get(fileName, null), stats); 1181 return stats; 1182 } catch (Exception e) { 1183 return null; 1184 } 1185 } 1186 } 1187 } 1188