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 17package com.android.server.usage; 18 19import android.app.usage.TimeSparseArray; 20import android.app.usage.UsageStatsManager; 21import android.util.AtomicFile; 22import android.util.Slog; 23 24import java.io.BufferedReader; 25import java.io.BufferedWriter; 26import java.io.File; 27import java.io.FileReader; 28import java.io.FileWriter; 29import java.io.FilenameFilter; 30import java.io.IOException; 31import java.util.ArrayList; 32import java.util.List; 33 34/** 35 * Provides an interface to query for UsageStat data from an XML database. 36 */ 37class UsageStatsDatabase { 38 private static final int CURRENT_VERSION = 2; 39 40 private static final String TAG = "UsageStatsDatabase"; 41 private static final boolean DEBUG = UsageStatsService.DEBUG; 42 private static final String BAK_SUFFIX = ".bak"; 43 private static final String CHECKED_IN_SUFFIX = UsageStatsXml.CHECKED_IN_SUFFIX; 44 45 private final Object mLock = new Object(); 46 private final File[] mIntervalDirs; 47 private final TimeSparseArray<AtomicFile>[] mSortedStatFiles; 48 private final UnixCalendar mCal; 49 private final File mVersionFile; 50 51 public UsageStatsDatabase(File dir) { 52 mIntervalDirs = new File[] { 53 new File(dir, "daily"), 54 new File(dir, "weekly"), 55 new File(dir, "monthly"), 56 new File(dir, "yearly"), 57 }; 58 mVersionFile = new File(dir, "version"); 59 mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length]; 60 mCal = new UnixCalendar(0); 61 } 62 63 /** 64 * Initialize any directories required and index what stats are available. 65 */ 66 public void init(long currentTimeMillis) { 67 synchronized (mLock) { 68 for (File f : mIntervalDirs) { 69 f.mkdirs(); 70 if (!f.exists()) { 71 throw new IllegalStateException("Failed to create directory " 72 + f.getAbsolutePath()); 73 } 74 } 75 76 checkVersionLocked(); 77 indexFilesLocked(); 78 79 // Delete files that are in the future. 80 for (TimeSparseArray<AtomicFile> files : mSortedStatFiles) { 81 final int startIndex = files.closestIndexOnOrAfter(currentTimeMillis); 82 if (startIndex < 0) { 83 continue; 84 } 85 86 final int fileCount = files.size(); 87 for (int i = startIndex; i < fileCount; i++) { 88 files.valueAt(i).delete(); 89 } 90 91 // Remove in a separate loop because any accesses (valueAt) 92 // will cause a gc in the SparseArray and mess up the order. 93 for (int i = startIndex; i < fileCount; i++) { 94 files.removeAt(i); 95 } 96 } 97 } 98 } 99 100 public interface CheckinAction { 101 boolean checkin(IntervalStats stats); 102 } 103 104 /** 105 * Calls {@link CheckinAction#checkin(IntervalStats)} on the given {@link CheckinAction} 106 * for all {@link IntervalStats} that haven't been checked-in. 107 * If any of the calls to {@link CheckinAction#checkin(IntervalStats)} returns false or throws 108 * an exception, the check-in will be aborted. 109 * 110 * @param checkinAction The callback to run when checking-in {@link IntervalStats}. 111 * @return true if the check-in succeeded. 112 */ 113 public boolean checkinDailyFiles(CheckinAction checkinAction) { 114 synchronized (mLock) { 115 final TimeSparseArray<AtomicFile> files = 116 mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY]; 117 final int fileCount = files.size(); 118 119 // We may have holes in the checkin (if there was an error) 120 // so find the last checked-in file and go from there. 121 int lastCheckin = -1; 122 for (int i = 0; i < fileCount - 1; i++) { 123 if (files.valueAt(i).getBaseFile().getPath().endsWith(CHECKED_IN_SUFFIX)) { 124 lastCheckin = i; 125 } 126 } 127 128 final int start = lastCheckin + 1; 129 if (start == fileCount - 1) { 130 return true; 131 } 132 133 try { 134 IntervalStats stats = new IntervalStats(); 135 for (int i = start; i < fileCount - 1; i++) { 136 UsageStatsXml.read(files.valueAt(i), stats); 137 if (!checkinAction.checkin(stats)) { 138 return false; 139 } 140 } 141 } catch (IOException e) { 142 Slog.e(TAG, "Failed to check-in", e); 143 return false; 144 } 145 146 // We have successfully checked-in the stats, so rename the files so that they 147 // are marked as checked-in. 148 for (int i = start; i < fileCount - 1; i++) { 149 final AtomicFile file = files.valueAt(i); 150 final File checkedInFile = new File( 151 file.getBaseFile().getPath() + CHECKED_IN_SUFFIX); 152 if (!file.getBaseFile().renameTo(checkedInFile)) { 153 // We must return success, as we've already marked some files as checked-in. 154 // It's better to repeat ourselves than to lose data. 155 Slog.e(TAG, "Failed to mark file " + file.getBaseFile().getPath() 156 + " as checked-in"); 157 return true; 158 } 159 160 // AtomicFile needs to set a new backup path with the same -c extension, so 161 // we replace the old AtomicFile with the updated one. 162 files.setValueAt(i, new AtomicFile(checkedInFile)); 163 } 164 } 165 return true; 166 } 167 168 private void indexFilesLocked() { 169 final FilenameFilter backupFileFilter = new FilenameFilter() { 170 @Override 171 public boolean accept(File dir, String name) { 172 return !name.endsWith(BAK_SUFFIX); 173 } 174 }; 175 176 // Index the available usage stat files on disk. 177 for (int i = 0; i < mSortedStatFiles.length; i++) { 178 if (mSortedStatFiles[i] == null) { 179 mSortedStatFiles[i] = new TimeSparseArray<>(); 180 } else { 181 mSortedStatFiles[i].clear(); 182 } 183 File[] files = mIntervalDirs[i].listFiles(backupFileFilter); 184 if (files != null) { 185 if (DEBUG) { 186 Slog.d(TAG, "Found " + files.length + " stat files for interval " + i); 187 } 188 189 for (File f : files) { 190 final AtomicFile af = new AtomicFile(f); 191 mSortedStatFiles[i].put(UsageStatsXml.parseBeginTime(af), af); 192 } 193 } 194 } 195 } 196 197 private void checkVersionLocked() { 198 int version; 199 try (BufferedReader reader = new BufferedReader(new FileReader(mVersionFile))) { 200 version = Integer.parseInt(reader.readLine()); 201 } catch (NumberFormatException | IOException e) { 202 version = 0; 203 } 204 205 if (version != CURRENT_VERSION) { 206 Slog.i(TAG, "Upgrading from version " + version + " to " + CURRENT_VERSION); 207 doUpgradeLocked(version); 208 209 try (BufferedWriter writer = new BufferedWriter(new FileWriter(mVersionFile))) { 210 writer.write(Integer.toString(CURRENT_VERSION)); 211 } catch (IOException e) { 212 Slog.e(TAG, "Failed to write new version"); 213 throw new RuntimeException(e); 214 } 215 } 216 } 217 218 private void doUpgradeLocked(int thisVersion) { 219 if (thisVersion < 2) { 220 // Delete all files if we are version 0. This is a pre-release version, 221 // so this is fine. 222 Slog.i(TAG, "Deleting all usage stats files"); 223 for (int i = 0; i < mIntervalDirs.length; i++) { 224 File[] files = mIntervalDirs[i].listFiles(); 225 if (files != null) { 226 for (File f : files) { 227 f.delete(); 228 } 229 } 230 } 231 } 232 } 233 234 public void onTimeChanged(long timeDiffMillis) { 235 synchronized (mLock) { 236 for (TimeSparseArray<AtomicFile> files : mSortedStatFiles) { 237 final int fileCount = files.size(); 238 for (int i = 0; i < fileCount; i++) { 239 final AtomicFile file = files.valueAt(i); 240 final long newTime = files.keyAt(i) + timeDiffMillis; 241 if (newTime < 0) { 242 Slog.i(TAG, "Deleting file " + file.getBaseFile().getAbsolutePath() 243 + " for it is in the future now."); 244 file.delete(); 245 } else { 246 try { 247 file.openRead().close(); 248 } catch (IOException e) { 249 // Ignore, this is just to make sure there are no backups. 250 } 251 252 String newName = Long.toString(newTime); 253 if (file.getBaseFile().getName().endsWith(CHECKED_IN_SUFFIX)) { 254 newName = newName + CHECKED_IN_SUFFIX; 255 } 256 257 final File newFile = new File(file.getBaseFile().getParentFile(), newName); 258 Slog.i(TAG, "Moving file " + file.getBaseFile().getAbsolutePath() + " to " 259 + newFile.getAbsolutePath()); 260 file.getBaseFile().renameTo(newFile); 261 } 262 } 263 files.clear(); 264 } 265 266 // Now re-index the new files. 267 indexFilesLocked(); 268 } 269 } 270 271 /** 272 * Get the latest stats that exist for this interval type. 273 */ 274 public IntervalStats getLatestUsageStats(int intervalType) { 275 synchronized (mLock) { 276 if (intervalType < 0 || intervalType >= mIntervalDirs.length) { 277 throw new IllegalArgumentException("Bad interval type " + intervalType); 278 } 279 280 final int fileCount = mSortedStatFiles[intervalType].size(); 281 if (fileCount == 0) { 282 return null; 283 } 284 285 try { 286 final AtomicFile f = mSortedStatFiles[intervalType].valueAt(fileCount - 1); 287 IntervalStats stats = new IntervalStats(); 288 UsageStatsXml.read(f, stats); 289 return stats; 290 } catch (IOException e) { 291 Slog.e(TAG, "Failed to read usage stats file", e); 292 } 293 } 294 return null; 295 } 296 297 /** 298 * Get the time at which the latest stats begin for this interval type. 299 */ 300 public long getLatestUsageStatsBeginTime(int intervalType) { 301 synchronized (mLock) { 302 if (intervalType < 0 || intervalType >= mIntervalDirs.length) { 303 throw new IllegalArgumentException("Bad interval type " + intervalType); 304 } 305 306 final int statsFileCount = mSortedStatFiles[intervalType].size(); 307 if (statsFileCount > 0) { 308 return mSortedStatFiles[intervalType].keyAt(statsFileCount - 1); 309 } 310 return -1; 311 } 312 } 313 314 /** 315 * Figures out what to extract from the given IntervalStats object. 316 */ 317 interface StatCombiner<T> { 318 319 /** 320 * Implementations should extract interesting from <code>stats</code> and add it 321 * to the <code>accumulatedResult</code> list. 322 * 323 * If the <code>stats</code> object is mutable, <code>mutable</code> will be true, 324 * which means you should make a copy of the data before adding it to the 325 * <code>accumulatedResult</code> list. 326 * 327 * @param stats The {@link IntervalStats} object selected. 328 * @param mutable Whether or not the data inside the stats object is mutable. 329 * @param accumulatedResult The list to which to add extracted data. 330 */ 331 void combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult); 332 } 333 334 /** 335 * Find all {@link IntervalStats} for the given range and interval type. 336 */ 337 public <T> List<T> queryUsageStats(int intervalType, long beginTime, long endTime, 338 StatCombiner<T> combiner) { 339 synchronized (mLock) { 340 if (intervalType < 0 || intervalType >= mIntervalDirs.length) { 341 throw new IllegalArgumentException("Bad interval type " + intervalType); 342 } 343 344 final TimeSparseArray<AtomicFile> intervalStats = mSortedStatFiles[intervalType]; 345 346 if (endTime <= beginTime) { 347 if (DEBUG) { 348 Slog.d(TAG, "endTime(" + endTime + ") <= beginTime(" + beginTime + ")"); 349 } 350 return null; 351 } 352 353 int startIndex = intervalStats.closestIndexOnOrBefore(beginTime); 354 if (startIndex < 0) { 355 // All the stats available have timestamps after beginTime, which means they all 356 // match. 357 startIndex = 0; 358 } 359 360 int endIndex = intervalStats.closestIndexOnOrBefore(endTime); 361 if (endIndex < 0) { 362 // All the stats start after this range ends, so nothing matches. 363 if (DEBUG) { 364 Slog.d(TAG, "No results for this range. All stats start after."); 365 } 366 return null; 367 } 368 369 if (intervalStats.keyAt(endIndex) == endTime) { 370 // The endTime is exclusive, so if we matched exactly take the one before. 371 endIndex--; 372 if (endIndex < 0) { 373 // All the stats start after this range ends, so nothing matches. 374 if (DEBUG) { 375 Slog.d(TAG, "No results for this range. All stats start after."); 376 } 377 return null; 378 } 379 } 380 381 try { 382 IntervalStats stats = new IntervalStats(); 383 ArrayList<T> results = new ArrayList<>(); 384 for (int i = startIndex; i <= endIndex; i++) { 385 final AtomicFile f = intervalStats.valueAt(i); 386 387 if (DEBUG) { 388 Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath()); 389 } 390 391 UsageStatsXml.read(f, stats); 392 if (beginTime < stats.endTime) { 393 combiner.combine(stats, false, results); 394 } 395 } 396 return results; 397 } catch (IOException e) { 398 Slog.e(TAG, "Failed to read usage stats file", e); 399 return null; 400 } 401 } 402 } 403 404 /** 405 * Find the interval that best matches this range. 406 * 407 * TODO(adamlesinski): Use endTimeStamp in best fit calculation. 408 */ 409 public int findBestFitBucket(long beginTimeStamp, long endTimeStamp) { 410 synchronized (mLock) { 411 int bestBucket = -1; 412 long smallestDiff = Long.MAX_VALUE; 413 for (int i = mSortedStatFiles.length - 1; i >= 0; i--) { 414 final int index = mSortedStatFiles[i].closestIndexOnOrBefore(beginTimeStamp); 415 int size = mSortedStatFiles[i].size(); 416 if (index >= 0 && index < size) { 417 // We have some results here, check if they are better than our current match. 418 long diff = Math.abs(mSortedStatFiles[i].keyAt(index) - beginTimeStamp); 419 if (diff < smallestDiff) { 420 smallestDiff = diff; 421 bestBucket = i; 422 } 423 } 424 } 425 return bestBucket; 426 } 427 } 428 429 /** 430 * Remove any usage stat files that are too old. 431 */ 432 public void prune(final long currentTimeMillis) { 433 synchronized (mLock) { 434 mCal.setTimeInMillis(currentTimeMillis); 435 mCal.addYears(-3); 436 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_YEARLY], 437 mCal.getTimeInMillis()); 438 439 mCal.setTimeInMillis(currentTimeMillis); 440 mCal.addMonths(-6); 441 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_MONTHLY], 442 mCal.getTimeInMillis()); 443 444 mCal.setTimeInMillis(currentTimeMillis); 445 mCal.addWeeks(-4); 446 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_WEEKLY], 447 mCal.getTimeInMillis()); 448 449 mCal.setTimeInMillis(currentTimeMillis); 450 mCal.addDays(-7); 451 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_DAILY], 452 mCal.getTimeInMillis()); 453 } 454 } 455 456 private static void pruneFilesOlderThan(File dir, long expiryTime) { 457 File[] files = dir.listFiles(); 458 if (files != null) { 459 for (File f : files) { 460 String path = f.getPath(); 461 if (path.endsWith(BAK_SUFFIX)) { 462 f = new File(path.substring(0, path.length() - BAK_SUFFIX.length())); 463 } 464 long beginTime = UsageStatsXml.parseBeginTime(f); 465 if (beginTime < expiryTime) { 466 new AtomicFile(f).delete(); 467 } 468 } 469 } 470 } 471 472 /** 473 * Update the stats in the database. They may not be written to disk immediately. 474 */ 475 public void putUsageStats(int intervalType, IntervalStats stats) throws IOException { 476 synchronized (mLock) { 477 if (intervalType < 0 || intervalType >= mIntervalDirs.length) { 478 throw new IllegalArgumentException("Bad interval type " + intervalType); 479 } 480 481 AtomicFile f = mSortedStatFiles[intervalType].get(stats.beginTime); 482 if (f == null) { 483 f = new AtomicFile(new File(mIntervalDirs[intervalType], 484 Long.toString(stats.beginTime))); 485 mSortedStatFiles[intervalType].put(stats.beginTime, f); 486 } 487 488 UsageStatsXml.write(f, stats); 489 stats.lastTimeSaved = f.getLastModifiedTime(); 490 } 491 } 492} 493