blob: 37340a4e194c1979e47ca78f59188021fec2277e [file] [log] [blame]
/**
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.android.server.usage;
import android.app.usage.TimeSparseArray;
import android.app.usage.UsageStatsManager;
import android.util.AtomicFile;
import android.util.Slog;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
/**
* Provides an interface to query for UsageStat data from an XML database.
*/
class UsageStatsDatabase {
private static final String TAG = "UsageStatsDatabase";
private static final boolean DEBUG = UsageStatsService.DEBUG;
private final Object mLock = new Object();
private final File[] mIntervalDirs;
private final TimeSparseArray<AtomicFile>[] mSortedStatFiles;
private final Calendar mCal;
public UsageStatsDatabase(File dir) {
mIntervalDirs = new File[] {
new File(dir, "daily"),
new File(dir, "weekly"),
new File(dir, "monthly"),
new File(dir, "yearly"),
};
mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length];
mCal = Calendar.getInstance();
}
/**
* Initialize any directories required and index what stats are available.
*/
void init() {
synchronized (mLock) {
for (File f : mIntervalDirs) {
f.mkdirs();
if (!f.exists()) {
throw new IllegalStateException("Failed to create directory "
+ f.getAbsolutePath());
}
}
final FilenameFilter backupFileFilter = new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return !name.endsWith(".bak");
}
};
// Index the available usage stat files on disk.
for (int i = 0; i < mSortedStatFiles.length; i++) {
mSortedStatFiles[i] = new TimeSparseArray<>();
File[] files = mIntervalDirs[i].listFiles(backupFileFilter);
if (files != null) {
if (DEBUG) {
Slog.d(TAG, "Found " + files.length + " stat files for interval " + i);
}
for (File f : files) {
mSortedStatFiles[i].put(Long.parseLong(f.getName()), new AtomicFile(f));
}
}
}
}
}
/**
* Get the latest stats that exist for this interval type.
*/
public IntervalStats getLatestUsageStats(int intervalType) {
synchronized (mLock) {
if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
throw new IllegalArgumentException("Bad interval type " + intervalType);
}
final int fileCount = mSortedStatFiles[intervalType].size();
if (fileCount == 0) {
return null;
}
try {
final AtomicFile f = mSortedStatFiles[intervalType].valueAt(fileCount - 1);
IntervalStats stats = new IntervalStats();
UsageStatsXml.read(f, stats);
return stats;
} catch (IOException e) {
Slog.e(TAG, "Failed to read usage stats file", e);
}
}
return null;
}
/**
* Get the time at which the latest stats begin for this interval type.
*/
public long getLatestUsageStatsBeginTime(int intervalType) {
synchronized (mLock) {
if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
throw new IllegalArgumentException("Bad interval type " + intervalType);
}
final int statsFileCount = mSortedStatFiles[intervalType].size();
if (statsFileCount > 0) {
return mSortedStatFiles[intervalType].keyAt(statsFileCount - 1);
}
return -1;
}
}
/**
* Figures out what to extract from the given IntervalStats object.
*/
interface StatCombiner<T> {
/**
* Implementations should extract interesting from <code>stats</code> and add it
* to the <code>accumulatedResult</code> list.
*
* If the <code>stats</code> object is mutable, <code>mutable</code> will be true,
* which means you should make a copy of the data before adding it to the
* <code>accumulatedResult</code> list.
*
* @param stats The {@link IntervalStats} object selected.
* @param mutable Whether or not the data inside the stats object is mutable.
* @param accumulatedResult The list to which to add extracted data.
*/
void combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult);
}
/**
* Find all {@link IntervalStats} for the given range and interval type.
*/
public <T> List<T> queryUsageStats(int intervalType, long beginTime, long endTime,
StatCombiner<T> combiner) {
synchronized (mLock) {
if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
throw new IllegalArgumentException("Bad interval type " + intervalType);
}
if (endTime < beginTime) {
return null;
}
final int startIndex = mSortedStatFiles[intervalType].closestIndexOnOrBefore(beginTime);
if (startIndex < 0) {
return null;
}
int endIndex = mSortedStatFiles[intervalType].closestIndexOnOrAfter(endTime);
if (endIndex < 0) {
endIndex = mSortedStatFiles[intervalType].size() - 1;
}
try {
IntervalStats stats = new IntervalStats();
ArrayList<T> results = new ArrayList<>();
for (int i = startIndex; i <= endIndex; i++) {
final AtomicFile f = mSortedStatFiles[intervalType].valueAt(i);
if (DEBUG) {
Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath());
}
UsageStatsXml.read(f, stats);
if (beginTime < stats.endTime) {
combiner.combine(stats, false, results);
}
}
return results;
} catch (IOException e) {
Slog.e(TAG, "Failed to read usage stats file", e);
return null;
}
}
}
/**
* Find the interval that best matches this range.
*
* TODO(adamlesinski): Use endTimeStamp in best fit calculation.
*/
public int findBestFitBucket(long beginTimeStamp, long endTimeStamp) {
synchronized (mLock) {
int bestBucket = -1;
long smallestDiff = Long.MAX_VALUE;
for (int i = mSortedStatFiles.length - 1; i >= 0; i--) {
final int index = mSortedStatFiles[i].closestIndexOnOrBefore(beginTimeStamp);
int size = mSortedStatFiles[i].size();
if (index >= 0 && index < size) {
// We have some results here, check if they are better than our current match.
long diff = Math.abs(mSortedStatFiles[i].keyAt(index) - beginTimeStamp);
if (diff < smallestDiff) {
smallestDiff = diff;
bestBucket = i;
}
}
}
return bestBucket;
}
}
/**
* Remove any usage stat files that are too old.
*/
public void prune() {
synchronized (mLock) {
long timeNow = System.currentTimeMillis();
mCal.setTimeInMillis(timeNow);
mCal.add(Calendar.YEAR, -3);
pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_YEARLY],
mCal.getTimeInMillis());
mCal.setTimeInMillis(timeNow);
mCal.add(Calendar.MONTH, -6);
pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_MONTHLY],
mCal.getTimeInMillis());
mCal.setTimeInMillis(timeNow);
mCal.add(Calendar.WEEK_OF_YEAR, -4);
pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_WEEKLY],
mCal.getTimeInMillis());
mCal.setTimeInMillis(timeNow);
mCal.add(Calendar.DAY_OF_YEAR, -7);
pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_DAILY],
mCal.getTimeInMillis());
}
}
private static void pruneFilesOlderThan(File dir, long expiryTime) {
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
long beginTime = Long.parseLong(f.getName());
if (beginTime < expiryTime) {
new AtomicFile(f).delete();
}
}
}
}
/**
* Update the stats in the database. They may not be written to disk immediately.
*/
public void putUsageStats(int intervalType, IntervalStats stats) throws IOException {
synchronized (mLock) {
if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
throw new IllegalArgumentException("Bad interval type " + intervalType);
}
AtomicFile f = mSortedStatFiles[intervalType].get(stats.beginTime);
if (f == null) {
f = new AtomicFile(new File(mIntervalDirs[intervalType],
Long.toString(stats.beginTime)));
mSortedStatFiles[intervalType].put(stats.beginTime, f);
}
UsageStatsXml.write(f, stats);
stats.lastTimeSaved = f.getLastModifiedTime();
}
}
}