Returns UsageEvents from previous days

- Avoid writing to disk when querying UsageStats.
- Use new UnixCalendar to avoid issues with Locale and TimeZone.

Bug: 16951313

Change-Id: I2473b8ef8dc1e2f6be22d4c689b96e346bdcafd5
This commit is contained in:
Adam Lesinski
2014-09-03 16:49:59 -07:00
parent 7f61e96db7
commit d26bea3a14
5 changed files with 305 additions and 177 deletions

View File

@@ -0,0 +1,99 @@
/**
* 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.UsageStatsManager;
/**
* A handy calendar object that knows nothing of Locale's or TimeZones. This simplifies
* interval book-keeping. It is *NOT* meant to be used as a user-facing calendar, as it has
* no concept of Locale or TimeZone.
*/
public class UnixCalendar {
private static final long DAY_IN_MILLIS = 24 * 60 * 60 * 1000;
private static final long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
private static final long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
private static final long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
private long mTime;
public UnixCalendar(long time) {
mTime = time;
}
public void truncateToDay() {
mTime -= mTime % DAY_IN_MILLIS;
}
public void truncateToWeek() {
mTime -= mTime % WEEK_IN_MILLIS;
}
public void truncateToMonth() {
mTime -= mTime % MONTH_IN_MILLIS;
}
public void truncateToYear() {
mTime -= mTime % YEAR_IN_MILLIS;
}
public void addDays(int val) {
mTime += val * DAY_IN_MILLIS;
}
public void addWeeks(int val) {
mTime += val * WEEK_IN_MILLIS;
}
public void addMonths(int val) {
mTime += val * MONTH_IN_MILLIS;
}
public void addYears(int val) {
mTime += val * YEAR_IN_MILLIS;
}
public void setTimeInMillis(long time) {
mTime = time;
}
public long getTimeInMillis() {
return mTime;
}
public static void truncateTo(UnixCalendar calendar, int intervalType) {
switch (intervalType) {
case UsageStatsManager.INTERVAL_YEARLY:
calendar.truncateToYear();
break;
case UsageStatsManager.INTERVAL_MONTHLY:
calendar.truncateToMonth();
break;
case UsageStatsManager.INTERVAL_WEEKLY:
calendar.truncateToWeek();
break;
case UsageStatsManager.INTERVAL_DAILY:
calendar.truncateToDay();
break;
default:
throw new UnsupportedOperationException("Can't truncate date to interval " +
intervalType);
}
}
}

View File

@@ -21,24 +21,30 @@ import android.app.usage.UsageStatsManager;
import android.util.AtomicFile;
import android.util.Slog;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
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 int CURRENT_VERSION = 1;
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;
private final UnixCalendar mCal;
private final File mVersionFile;
public UsageStatsDatabase(File dir) {
mIntervalDirs = new File[] {
@@ -47,8 +53,9 @@ class UsageStatsDatabase {
new File(dir, "monthly"),
new File(dir, "yearly"),
};
mVersionFile = new File(dir, "version");
mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length];
mCal = Calendar.getInstance();
mCal = new UnixCalendar(0);
}
/**
@@ -64,6 +71,8 @@ class UsageStatsDatabase {
}
}
checkVersionLocked();
final FilenameFilter backupFileFilter = new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
@@ -88,6 +97,43 @@ class UsageStatsDatabase {
}
}
private void checkVersionLocked() {
int version;
try (BufferedReader reader = new BufferedReader(new FileReader(mVersionFile))) {
version = Integer.parseInt(reader.readLine());
} catch (NumberFormatException | IOException e) {
version = 0;
}
if (version != CURRENT_VERSION) {
Slog.i(TAG, "Upgrading from version " + version + " to " + CURRENT_VERSION);
doUpgradeLocked(version, CURRENT_VERSION);
try (BufferedWriter writer = new BufferedWriter(new FileWriter(mVersionFile))) {
writer.write(Integer.toString(CURRENT_VERSION));
} catch (IOException e) {
Slog.e(TAG, "Failed to write new version");
throw new RuntimeException(e);
}
}
}
private void doUpgradeLocked(int thisVersion, int nextVersion) {
if (thisVersion == 0) {
// Delete all files if we are version 0. This is a pre-release version,
// so this is fine.
Slog.i(TAG, "Deleting all usage stats files");
for (int i = 0; i < mIntervalDirs.length; i++) {
File[] files = mIntervalDirs[i].listFiles();
if (files != null) {
for (File f : files) {
f.delete();
}
}
}
}
}
/**
* Get the latest stats that exist for this interval type.
*/
@@ -161,25 +207,48 @@ class UsageStatsDatabase {
throw new IllegalArgumentException("Bad interval type " + intervalType);
}
if (endTime < beginTime) {
final TimeSparseArray<AtomicFile> intervalStats = mSortedStatFiles[intervalType];
if (endTime <= beginTime) {
if (DEBUG) {
Slog.d(TAG, "endTime(" + endTime + ") <= beginTime(" + beginTime + ")");
}
return null;
}
final int startIndex = mSortedStatFiles[intervalType].closestIndexOnOrBefore(beginTime);
int startIndex = intervalStats.closestIndexOnOrBefore(beginTime);
if (startIndex < 0) {
// All the stats available have timestamps after beginTime, which means they all
// match.
startIndex = 0;
}
int endIndex = intervalStats.closestIndexOnOrBefore(endTime);
if (endIndex < 0) {
// All the stats start after this range ends, so nothing matches.
if (DEBUG) {
Slog.d(TAG, "No results for this range. All stats start after.");
}
return null;
}
int endIndex = mSortedStatFiles[intervalType].closestIndexOnOrAfter(endTime);
if (endIndex < 0) {
endIndex = mSortedStatFiles[intervalType].size() - 1;
if (intervalStats.keyAt(endIndex) == endTime) {
// The endTime is exclusive, so if we matched exactly take the one before.
endIndex--;
if (endIndex < 0) {
// All the stats start after this range ends, so nothing matches.
if (DEBUG) {
Slog.d(TAG, "No results for this range. All stats start after.");
}
return null;
}
}
try {
IntervalStats stats = new IntervalStats();
ArrayList<T> results = new ArrayList<>();
for (int i = startIndex; i <= endIndex; i++) {
final AtomicFile f = mSortedStatFiles[intervalType].valueAt(i);
final AtomicFile f = intervalStats.valueAt(i);
if (DEBUG) {
Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath());
@@ -230,22 +299,22 @@ class UsageStatsDatabase {
synchronized (mLock) {
long timeNow = System.currentTimeMillis();
mCal.setTimeInMillis(timeNow);
mCal.add(Calendar.YEAR, -3);
mCal.addYears(-3);
pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_YEARLY],
mCal.getTimeInMillis());
mCal.setTimeInMillis(timeNow);
mCal.add(Calendar.MONTH, -6);
mCal.addMonths(-6);
pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_MONTHLY],
mCal.getTimeInMillis());
mCal.setTimeInMillis(timeNow);
mCal.add(Calendar.WEEK_OF_YEAR, -4);
mCal.addWeeks(-4);
pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_WEEKLY],
mCal.getTimeInMillis());
mCal.setTimeInMillis(timeNow);
mCal.add(Calendar.DAY_OF_YEAR, -7);
mCal.addDays(-7);
pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_DAILY],
mCal.getTimeInMillis());
}

View File

@@ -62,9 +62,7 @@ public class UsageStatsService extends SystemService implements
static final boolean DEBUG = false;
private static final long TEN_SECONDS = 10 * 1000;
private static final long TWENTY_MINUTES = 20 * 60 * 1000;
private static final long TWO_MINUTES = 2 * 60 * 1000;
private static final long FLUSH_INTERVAL = DEBUG ? TEN_SECONDS : TWENTY_MINUTES;
private static final long END_TIME_DELAY = DEBUG ? 0 : TWO_MINUTES;
// Handler message types.
static final int MSG_REPORT_EVENT = 0;
@@ -178,7 +176,7 @@ public class UsageStatsService extends SystemService implements
}
/**
* Called by the Bunder stub
* Called by the Binder stub
*/
void shutdown() {
synchronized (mLock) {

View File

@@ -1,63 +0,0 @@
/**
* 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.UsageStatsManager;
import java.util.Calendar;
/**
* A collection of utility methods used by the UsageStatsService and accompanying classes.
*/
final class UsageStatsUtils {
private UsageStatsUtils() {}
/**
* Truncates the date to the given UsageStats bucket. For example, if the bucket is
* {@link UsageStatsManager#INTERVAL_YEARLY}, the date is truncated to the 1st day of the year,
* with the time set to 00:00:00.
*
* @param bucket The UsageStats bucket to truncate to.
* @param cal The date to truncate.
*/
public static void truncateDateTo(int bucket, Calendar cal) {
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
switch (bucket) {
case UsageStatsManager.INTERVAL_YEARLY:
cal.set(Calendar.DAY_OF_YEAR, 0);
break;
case UsageStatsManager.INTERVAL_MONTHLY:
cal.set(Calendar.DAY_OF_MONTH, 0);
break;
case UsageStatsManager.INTERVAL_WEEKLY:
cal.set(Calendar.DAY_OF_WEEK, 0);
break;
case UsageStatsManager.INTERVAL_DAILY:
break;
default:
throw new UnsupportedOperationException("Can't truncate date to bucket " + bucket);
}
}
}

View File

@@ -32,7 +32,6 @@ import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
/**
@@ -47,7 +46,7 @@ class UserUsageStatsService {
private final UsageStatsDatabase mDatabase;
private final IntervalStats[] mCurrentStats;
private boolean mStatsChanged = false;
private final Calendar mDailyExpiryDate;
private final UnixCalendar mDailyExpiryDate;
private final StatsUpdatedListener mListener;
private final String mLogPrefix;
@@ -56,7 +55,7 @@ class UserUsageStatsService {
}
UserUsageStatsService(int userId, File usageStatsDir, StatsUpdatedListener listener) {
mDailyExpiryDate = Calendar.getInstance();
mDailyExpiryDate = new UnixCalendar(0);
mDatabase = new UsageStatsDatabase(usageStatsDir);
mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
mListener = listener;
@@ -66,6 +65,7 @@ class UserUsageStatsService {
void init() {
mDatabase.init();
final long timeNow = System.currentTimeMillis();
int nullCount = 0;
for (int i = 0; i < mCurrentStats.length; i++) {
mCurrentStats[i] = mDatabase.getLatestUsageStats(i);
@@ -73,6 +73,11 @@ class UserUsageStatsService {
// Find out how many intervals we don't have data for.
// Ideally it should be all or none.
nullCount++;
} else if (mCurrentStats[i].beginTime > timeNow) {
Slog.e(TAG, mLogPrefix + "Interval " + i + " has stat in the future " +
mCurrentStats[i].beginTime);
mCurrentStats[i] = null;
nullCount++;
}
}
@@ -94,10 +99,11 @@ class UserUsageStatsService {
// that is reported.
mDailyExpiryDate.setTimeInMillis(
mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime);
mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1);
UsageStatsUtils.truncateDateTo(UsageStatsManager.INTERVAL_DAILY, mDailyExpiryDate);
Slog.i(TAG, mLogPrefix + "Rollover scheduled for "
+ sDateFormat.format(mDailyExpiryDate.getTime()));
mDailyExpiryDate.addDays(1);
mDailyExpiryDate.truncateToDay();
Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) +
"(" + mDailyExpiryDate.getTimeInMillis() + ")");
}
// Now close off any events that were open at the time this was saved.
@@ -195,49 +201,68 @@ class UserUsageStatsService {
* and bucket, then calls the {@link com.android.server.usage.UsageStatsDatabase.StatCombiner}
* provided to select the stats to use from the IntervalStats object.
*/
private <T> List<T> queryStats(int bucketType, long beginTime, long endTime,
private <T> List<T> queryStats(int intervalType, final long beginTime, final long endTime,
StatCombiner<T> combiner) {
if (bucketType == UsageStatsManager.INTERVAL_BEST) {
bucketType = mDatabase.findBestFitBucket(beginTime, endTime);
if (intervalType == UsageStatsManager.INTERVAL_BEST) {
intervalType = mDatabase.findBestFitBucket(beginTime, endTime);
if (intervalType < 0) {
// Nothing saved to disk yet, so every stat is just as equal (no rollover has
// occurred.
intervalType = UsageStatsManager.INTERVAL_DAILY;
}
}
if (bucketType < 0 || bucketType >= mCurrentStats.length) {
if (intervalType < 0 || intervalType >= mCurrentStats.length) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Bad bucketType used " + bucketType);
Slog.d(TAG, mLogPrefix + "Bad intervalType used " + intervalType);
}
return null;
}
if (beginTime >= mCurrentStats[bucketType].endTime) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is "
+ mCurrentStats[bucketType].endTime);
}
// Nothing newer available.
return null;
} else if (beginTime >= mCurrentStats[bucketType].beginTime) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Returning in-memory stats for bucket " + bucketType);
}
// Fast path for retrieving in-memory state.
ArrayList<T> results = new ArrayList<>();
combiner.combine(mCurrentStats[bucketType], true, results);
return results;
}
// Flush any changes that were made to disk before we do a disk query.
// If we're not grabbing the ongoing stats, no need to persist.
persistActiveStats();
final IntervalStats currentStats = mCurrentStats[intervalType];
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "SELECT * FROM " + bucketType + " WHERE beginTime >= "
Slog.d(TAG, mLogPrefix + "SELECT * FROM " + intervalType + " WHERE beginTime >= "
+ beginTime + " AND endTime < " + endTime);
}
final List<T> results = mDatabase.queryUsageStats(bucketType, beginTime, endTime, combiner);
if (beginTime >= currentStats.endTime) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is "
+ currentStats.endTime);
}
// Nothing newer available.
return null;
}
// Truncate the endTime to just before the in-memory stats. Then, we'll append the
// in-memory stats to the results (if necessary) so as to avoid writing to disk too
// often.
final long truncatedEndTime = Math.min(currentStats.beginTime, endTime);
// Get the stats from disk.
List<T> results = mDatabase.queryUsageStats(intervalType, beginTime,
truncatedEndTime, combiner);
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Results: " + (results == null ? 0 : results.size()));
Slog.d(TAG, "Got " + (results != null ? results.size() : 0) + " results from disk");
Slog.d(TAG, "Current stats beginTime=" + currentStats.beginTime +
" endTime=" + currentStats.endTime);
}
// Now check if the in-memory stats match the range and add them if they do.
if (beginTime < currentStats.endTime && endTime > currentStats.beginTime) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Returning in-memory stats");
}
if (results == null) {
results = new ArrayList<>();
}
combiner.combine(currentStats, true, results);
}
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Results: " + (results != null ? results.size() : 0));
}
return results;
}
@@ -250,44 +275,45 @@ class UserUsageStatsService {
return queryStats(bucketType, beginTime, endTime, sConfigStatsCombiner);
}
UsageEvents queryEvents(long beginTime, long endTime) {
if (endTime > mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime) {
if (beginTime > mCurrentStats[UsageStatsManager.INTERVAL_DAILY].endTime) {
return null;
}
UsageEvents queryEvents(final long beginTime, final long endTime) {
final ArraySet<String> names = new ArraySet<>();
List<UsageEvents.Event> results = queryStats(UsageStatsManager.INTERVAL_DAILY,
beginTime, endTime, new StatCombiner<UsageEvents.Event>() {
@Override
public void combine(IntervalStats stats, boolean mutable,
List<UsageEvents.Event> accumulatedResult) {
if (stats.events == null) {
return;
}
TimeSparseArray<UsageEvents.Event> events =
mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events;
if (events == null) {
return null;
}
final int startIndex = stats.events.closestIndexOnOrAfter(beginTime);
if (startIndex < 0) {
return;
}
final int startIndex = events.closestIndexOnOrAfter(beginTime);
if (startIndex < 0) {
return null;
}
final int size = stats.events.size();
for (int i = startIndex; i < size; i++) {
if (stats.events.keyAt(i) >= endTime) {
return;
}
ArraySet<String> names = new ArraySet<>();
ArrayList<UsageEvents.Event> results = new ArrayList<>();
final int size = events.size();
for (int i = startIndex; i < size; i++) {
if (events.keyAt(i) >= endTime) {
break;
}
final UsageEvents.Event event = events.valueAt(i);
names.add(event.mPackage);
if (event.mClass != null) {
names.add(event.mClass);
}
results.add(event);
}
String[] table = names.toArray(new String[names.size()]);
Arrays.sort(table);
return new UsageEvents(results, table);
final UsageEvents.Event event = stats.events.valueAt(i);
names.add(event.mPackage);
if (event.mClass != null) {
names.add(event.mClass);
}
accumulatedResult.add(event);
}
}
});
if (results == null || results.isEmpty()) {
return null;
}
// TODO(adamlesinski): Query the previous days.
return null;
String[] table = names.toArray(new String[names.size()]);
Arrays.sort(table);
return new UsageEvents(results, table);
}
void persistActiveStats() {
@@ -360,54 +386,53 @@ class UserUsageStatsService {
private void loadActiveStats() {
final long timeNow = System.currentTimeMillis();
Calendar tempCal = mDailyExpiryDate;
for (int bucketType = 0; bucketType < mCurrentStats.length; bucketType++) {
final UnixCalendar tempCal = mDailyExpiryDate;
for (int intervalType = 0; intervalType < mCurrentStats.length; intervalType++) {
tempCal.setTimeInMillis(timeNow);
UsageStatsUtils.truncateDateTo(bucketType, tempCal);
UnixCalendar.truncateTo(tempCal, intervalType);
if (mCurrentStats[bucketType] != null &&
mCurrentStats[bucketType].beginTime == tempCal.getTimeInMillis()) {
if (mCurrentStats[intervalType] != null &&
mCurrentStats[intervalType].beginTime == tempCal.getTimeInMillis()) {
// These are the same, no need to load them (in memory stats are always newer
// than persisted stats).
continue;
}
final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(bucketType);
if (lastBeginTime >= tempCal.getTimeInMillis()) {
final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(intervalType);
if (lastBeginTime > timeNow) {
Slog.e(TAG, mLogPrefix + "Latest usage stats for interval " +
intervalType + " begins in the future");
mCurrentStats[intervalType] = null;
} else if (lastBeginTime >= tempCal.getTimeInMillis()) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Loading existing stats (" + lastBeginTime +
") for bucket " + bucketType);
}
mCurrentStats[bucketType] = mDatabase.getLatestUsageStats(bucketType);
if (DEBUG) {
if (mCurrentStats[bucketType] != null) {
Slog.d(TAG, mLogPrefix + "Found " +
(mCurrentStats[bucketType].events == null ?
0 : mCurrentStats[bucketType].events.size()) +
" events");
}
Slog.d(TAG, mLogPrefix + "Loading existing stats @ " +
sDateFormat.format(lastBeginTime) + "(" + lastBeginTime +
") for interval " + intervalType);
}
mCurrentStats[intervalType] = mDatabase.getLatestUsageStats(intervalType);
} else {
mCurrentStats[bucketType] = null;
mCurrentStats[intervalType] = null;
}
if (mCurrentStats[bucketType] == null) {
if (mCurrentStats[intervalType] == null) {
if (DEBUG) {
Slog.d(TAG, "Creating new stats (" + tempCal.getTimeInMillis() +
") for bucket " + bucketType);
Slog.d(TAG, "Creating new stats @ " +
sDateFormat.format(tempCal.getTimeInMillis()) + "(" +
tempCal.getTimeInMillis() + ") for interval " + intervalType);
}
mCurrentStats[bucketType] = new IntervalStats();
mCurrentStats[bucketType].beginTime = tempCal.getTimeInMillis();
mCurrentStats[bucketType].endTime = timeNow;
mCurrentStats[intervalType] = new IntervalStats();
mCurrentStats[intervalType].beginTime = tempCal.getTimeInMillis();
mCurrentStats[intervalType].endTime = timeNow;
}
}
mStatsChanged = false;
mDailyExpiryDate.setTimeInMillis(timeNow);
mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1);
UsageStatsUtils.truncateDateTo(UsageStatsManager.INTERVAL_DAILY, mDailyExpiryDate);
Slog.i(TAG, mLogPrefix + "Rollover scheduled for "
+ sDateFormat.format(mDailyExpiryDate.getTime()));
mDailyExpiryDate.addDays(1);
mDailyExpiryDate.truncateToDay();
Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) + "(" +
tempCal.getTimeInMillis() + ")");
}