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