UsageStatsDatabase.java revision 7f61e96db7c90c1f4418359672aa4656aebee500
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.File; 25import java.io.FilenameFilter; 26import java.io.IOException; 27import java.util.ArrayList; 28import java.util.Calendar; 29import java.util.List; 30 31/** 32 * Provides an interface to query for UsageStat data from an XML database. 33 */ 34class UsageStatsDatabase { 35 private static final String TAG = "UsageStatsDatabase"; 36 private static final boolean DEBUG = UsageStatsService.DEBUG; 37 38 private final Object mLock = new Object(); 39 private final File[] mIntervalDirs; 40 private final TimeSparseArray<AtomicFile>[] mSortedStatFiles; 41 private final Calendar mCal; 42 43 public UsageStatsDatabase(File dir) { 44 mIntervalDirs = new File[] { 45 new File(dir, "daily"), 46 new File(dir, "weekly"), 47 new File(dir, "monthly"), 48 new File(dir, "yearly"), 49 }; 50 mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length]; 51 mCal = Calendar.getInstance(); 52 } 53 54 /** 55 * Initialize any directories required and index what stats are available. 56 */ 57 void init() { 58 synchronized (mLock) { 59 for (File f : mIntervalDirs) { 60 f.mkdirs(); 61 if (!f.exists()) { 62 throw new IllegalStateException("Failed to create directory " 63 + f.getAbsolutePath()); 64 } 65 } 66 67 final FilenameFilter backupFileFilter = new FilenameFilter() { 68 @Override 69 public boolean accept(File dir, String name) { 70 return !name.endsWith(".bak"); 71 } 72 }; 73 74 // Index the available usage stat files on disk. 75 for (int i = 0; i < mSortedStatFiles.length; i++) { 76 mSortedStatFiles[i] = new TimeSparseArray<>(); 77 File[] files = mIntervalDirs[i].listFiles(backupFileFilter); 78 if (files != null) { 79 if (DEBUG) { 80 Slog.d(TAG, "Found " + files.length + " stat files for interval " + i); 81 } 82 83 for (File f : files) { 84 mSortedStatFiles[i].put(Long.parseLong(f.getName()), new AtomicFile(f)); 85 } 86 } 87 } 88 } 89 } 90 91 /** 92 * Get the latest stats that exist for this interval type. 93 */ 94 public IntervalStats getLatestUsageStats(int intervalType) { 95 synchronized (mLock) { 96 if (intervalType < 0 || intervalType >= mIntervalDirs.length) { 97 throw new IllegalArgumentException("Bad interval type " + intervalType); 98 } 99 100 final int fileCount = mSortedStatFiles[intervalType].size(); 101 if (fileCount == 0) { 102 return null; 103 } 104 105 try { 106 final AtomicFile f = mSortedStatFiles[intervalType].valueAt(fileCount - 1); 107 IntervalStats stats = new IntervalStats(); 108 UsageStatsXml.read(f, stats); 109 return stats; 110 } catch (IOException e) { 111 Slog.e(TAG, "Failed to read usage stats file", e); 112 } 113 } 114 return null; 115 } 116 117 /** 118 * Get the time at which the latest stats begin for this interval type. 119 */ 120 public long getLatestUsageStatsBeginTime(int intervalType) { 121 synchronized (mLock) { 122 if (intervalType < 0 || intervalType >= mIntervalDirs.length) { 123 throw new IllegalArgumentException("Bad interval type " + intervalType); 124 } 125 126 final int statsFileCount = mSortedStatFiles[intervalType].size(); 127 if (statsFileCount > 0) { 128 return mSortedStatFiles[intervalType].keyAt(statsFileCount - 1); 129 } 130 return -1; 131 } 132 } 133 134 /** 135 * Figures out what to extract from the given IntervalStats object. 136 */ 137 interface StatCombiner<T> { 138 139 /** 140 * Implementations should extract interesting from <code>stats</code> and add it 141 * to the <code>accumulatedResult</code> list. 142 * 143 * If the <code>stats</code> object is mutable, <code>mutable</code> will be true, 144 * which means you should make a copy of the data before adding it to the 145 * <code>accumulatedResult</code> list. 146 * 147 * @param stats The {@link IntervalStats} object selected. 148 * @param mutable Whether or not the data inside the stats object is mutable. 149 * @param accumulatedResult The list to which to add extracted data. 150 */ 151 void combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult); 152 } 153 154 /** 155 * Find all {@link IntervalStats} for the given range and interval type. 156 */ 157 public <T> List<T> queryUsageStats(int intervalType, long beginTime, long endTime, 158 StatCombiner<T> combiner) { 159 synchronized (mLock) { 160 if (intervalType < 0 || intervalType >= mIntervalDirs.length) { 161 throw new IllegalArgumentException("Bad interval type " + intervalType); 162 } 163 164 if (endTime < beginTime) { 165 return null; 166 } 167 168 final int startIndex = mSortedStatFiles[intervalType].closestIndexOnOrBefore(beginTime); 169 if (startIndex < 0) { 170 return null; 171 } 172 173 int endIndex = mSortedStatFiles[intervalType].closestIndexOnOrAfter(endTime); 174 if (endIndex < 0) { 175 endIndex = mSortedStatFiles[intervalType].size() - 1; 176 } 177 178 try { 179 IntervalStats stats = new IntervalStats(); 180 ArrayList<T> results = new ArrayList<>(); 181 for (int i = startIndex; i <= endIndex; i++) { 182 final AtomicFile f = mSortedStatFiles[intervalType].valueAt(i); 183 184 if (DEBUG) { 185 Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath()); 186 } 187 188 UsageStatsXml.read(f, stats); 189 if (beginTime < stats.endTime) { 190 combiner.combine(stats, false, results); 191 } 192 } 193 return results; 194 } catch (IOException e) { 195 Slog.e(TAG, "Failed to read usage stats file", e); 196 return null; 197 } 198 } 199 } 200 201 /** 202 * Find the interval that best matches this range. 203 * 204 * TODO(adamlesinski): Use endTimeStamp in best fit calculation. 205 */ 206 public int findBestFitBucket(long beginTimeStamp, long endTimeStamp) { 207 synchronized (mLock) { 208 int bestBucket = -1; 209 long smallestDiff = Long.MAX_VALUE; 210 for (int i = mSortedStatFiles.length - 1; i >= 0; i--) { 211 final int index = mSortedStatFiles[i].closestIndexOnOrBefore(beginTimeStamp); 212 int size = mSortedStatFiles[i].size(); 213 if (index >= 0 && index < size) { 214 // We have some results here, check if they are better than our current match. 215 long diff = Math.abs(mSortedStatFiles[i].keyAt(index) - beginTimeStamp); 216 if (diff < smallestDiff) { 217 smallestDiff = diff; 218 bestBucket = i; 219 } 220 } 221 } 222 return bestBucket; 223 } 224 } 225 226 /** 227 * Remove any usage stat files that are too old. 228 */ 229 public void prune() { 230 synchronized (mLock) { 231 long timeNow = System.currentTimeMillis(); 232 mCal.setTimeInMillis(timeNow); 233 mCal.add(Calendar.YEAR, -3); 234 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_YEARLY], 235 mCal.getTimeInMillis()); 236 237 mCal.setTimeInMillis(timeNow); 238 mCal.add(Calendar.MONTH, -6); 239 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_MONTHLY], 240 mCal.getTimeInMillis()); 241 242 mCal.setTimeInMillis(timeNow); 243 mCal.add(Calendar.WEEK_OF_YEAR, -4); 244 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_WEEKLY], 245 mCal.getTimeInMillis()); 246 247 mCal.setTimeInMillis(timeNow); 248 mCal.add(Calendar.DAY_OF_YEAR, -7); 249 pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_DAILY], 250 mCal.getTimeInMillis()); 251 } 252 } 253 254 private static void pruneFilesOlderThan(File dir, long expiryTime) { 255 File[] files = dir.listFiles(); 256 if (files != null) { 257 for (File f : files) { 258 long beginTime = Long.parseLong(f.getName()); 259 if (beginTime < expiryTime) { 260 new AtomicFile(f).delete(); 261 } 262 } 263 } 264 } 265 266 /** 267 * Update the stats in the database. They may not be written to disk immediately. 268 */ 269 public void putUsageStats(int intervalType, IntervalStats stats) throws IOException { 270 synchronized (mLock) { 271 if (intervalType < 0 || intervalType >= mIntervalDirs.length) { 272 throw new IllegalArgumentException("Bad interval type " + intervalType); 273 } 274 275 AtomicFile f = mSortedStatFiles[intervalType].get(stats.beginTime); 276 if (f == null) { 277 f = new AtomicFile(new File(mIntervalDirs[intervalType], 278 Long.toString(stats.beginTime))); 279 mSortedStatFiles[intervalType].put(stats.beginTime, f); 280 } 281 282 UsageStatsXml.write(f, stats); 283 stats.lastTimeSaved = f.getLastModifiedTime(); 284 } 285 } 286} 287