Loading services/usage/java/com/android/server/usage/UsageStatsUtils.java→services/usage/java/com/android/server/usage/UnixCalendar.java +99 −0 Original line number Diff line number Diff line Loading @@ -13,51 +13,87 @@ * 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. * 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. */ final class UsageStatsUtils { private UsageStatsUtils() {} 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; /** * 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); 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; } switch (bucket) { public static void truncateTo(UnixCalendar calendar, int intervalType) { switch (intervalType) { case UsageStatsManager.INTERVAL_YEARLY: cal.set(Calendar.DAY_OF_YEAR, 0); calendar.truncateToYear(); break; case UsageStatsManager.INTERVAL_MONTHLY: cal.set(Calendar.DAY_OF_MONTH, 0); calendar.truncateToMonth(); break; case UsageStatsManager.INTERVAL_WEEKLY: cal.set(Calendar.DAY_OF_WEEK, 0); calendar.truncateToWeek(); break; case UsageStatsManager.INTERVAL_DAILY: calendar.truncateToDay(); break; default: throw new UnsupportedOperationException("Can't truncate date to bucket " + bucket); throw new UnsupportedOperationException("Can't truncate date to interval " + intervalType); } } } services/usage/java/com/android/server/usage/UsageStatsDatabase.java +82 −13 Original line number Diff line number Diff line Loading @@ -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[] { Loading @@ -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); } /** Loading @@ -64,6 +71,8 @@ class UsageStatsDatabase { } } checkVersionLocked(); final FilenameFilter backupFileFilter = new FilenameFilter() { @Override public boolean accept(File dir, String name) { Loading @@ -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. */ Loading Loading @@ -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 (intervalStats.keyAt(endIndex) == endTime) { // The endTime is exclusive, so if we matched exactly take the one before. endIndex--; if (endIndex < 0) { endIndex = mSortedStatFiles[intervalType].size() - 1; // 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()); Loading Loading @@ -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()); } Loading services/usage/java/com/android/server/usage/UsageStatsService.java +1 −3 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -178,7 +176,7 @@ public class UsageStatsService extends SystemService implements } /** * Called by the Bunder stub * Called by the Binder stub */ void shutdown() { synchronized (mLock) { Loading services/usage/java/com/android/server/usage/UserUsageStatsService.java +119 −94 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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; Loading @@ -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; Loading @@ -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); Loading @@ -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++; } } Loading @@ -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. Loading Loading @@ -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) { final IntervalStats currentStats = mCurrentStats[intervalType]; if (DEBUG) { Slog.d(TAG, mLogPrefix + "SELECT * FROM " + intervalType + " WHERE beginTime >= " + beginTime + " AND endTime < " + endTime); } if (beginTime >= currentStats.endTime) { if (DEBUG) { Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is " + mCurrentStats[bucketType].endTime); + 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); } else if (beginTime >= mCurrentStats[bucketType].beginTime) { // Get the stats from disk. List<T> results = mDatabase.queryUsageStats(intervalType, beginTime, truncatedEndTime, combiner); 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; Slog.d(TAG, "Got " + (results != null ? results.size() : 0) + " results from disk"); Slog.d(TAG, "Current stats beginTime=" + currentStats.beginTime + " endTime=" + currentStats.endTime); } // 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(); // 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 + "SELECT * FROM " + bucketType + " WHERE beginTime >= " + beginTime + " AND endTime < " + endTime); Slog.d(TAG, mLogPrefix + "Returning in-memory stats"); } if (results == null) { results = new ArrayList<>(); } combiner.combine(currentStats, true, results); } final List<T> results = mDatabase.queryUsageStats(bucketType, beginTime, endTime, combiner); if (DEBUG) { Slog.d(TAG, mLogPrefix + "Results: " + (results == null ? 0 : results.size())); Slog.d(TAG, mLogPrefix + "Results: " + (results != null ? results.size() : 0)); } return results; } Loading @@ -250,46 +275,47 @@ 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; } TimeSparseArray<UsageEvents.Event> events = mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events; if (events == null) { 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; } final int startIndex = events.closestIndexOnOrAfter(beginTime); final int startIndex = stats.events.closestIndexOnOrAfter(beginTime); if (startIndex < 0) { return null; return; } ArraySet<String> names = new ArraySet<>(); ArrayList<UsageEvents.Event> results = new ArrayList<>(); final int size = events.size(); final int size = stats.events.size(); for (int i = startIndex; i < size; i++) { if (events.keyAt(i) >= endTime) { break; if (stats.events.keyAt(i) >= endTime) { return; } final UsageEvents.Event event = events.valueAt(i); final UsageEvents.Event event = stats.events.valueAt(i); names.add(event.mPackage); if (event.mClass != null) { names.add(event.mClass); } results.add(event); accumulatedResult.add(event); } String[] table = names.toArray(new String[names.size()]); Arrays.sort(table); return new UsageEvents(results, table); } }); // TODO(adamlesinski): Query the previous days. if (results == null || results.isEmpty()) { return null; } String[] table = names.toArray(new String[names.size()]); Arrays.sort(table); return new UsageEvents(results, table); } void persistActiveStats() { if (mStatsChanged) { Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk"); Loading Loading @@ -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() + ")"); } Loading Loading
services/usage/java/com/android/server/usage/UsageStatsUtils.java→services/usage/java/com/android/server/usage/UnixCalendar.java +99 −0 Original line number Diff line number Diff line Loading @@ -13,51 +13,87 @@ * 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. * 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. */ final class UsageStatsUtils { private UsageStatsUtils() {} 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; /** * 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); 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; } switch (bucket) { public static void truncateTo(UnixCalendar calendar, int intervalType) { switch (intervalType) { case UsageStatsManager.INTERVAL_YEARLY: cal.set(Calendar.DAY_OF_YEAR, 0); calendar.truncateToYear(); break; case UsageStatsManager.INTERVAL_MONTHLY: cal.set(Calendar.DAY_OF_MONTH, 0); calendar.truncateToMonth(); break; case UsageStatsManager.INTERVAL_WEEKLY: cal.set(Calendar.DAY_OF_WEEK, 0); calendar.truncateToWeek(); break; case UsageStatsManager.INTERVAL_DAILY: calendar.truncateToDay(); break; default: throw new UnsupportedOperationException("Can't truncate date to bucket " + bucket); throw new UnsupportedOperationException("Can't truncate date to interval " + intervalType); } } }
services/usage/java/com/android/server/usage/UsageStatsDatabase.java +82 −13 Original line number Diff line number Diff line Loading @@ -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[] { Loading @@ -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); } /** Loading @@ -64,6 +71,8 @@ class UsageStatsDatabase { } } checkVersionLocked(); final FilenameFilter backupFileFilter = new FilenameFilter() { @Override public boolean accept(File dir, String name) { Loading @@ -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. */ Loading Loading @@ -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 (intervalStats.keyAt(endIndex) == endTime) { // The endTime is exclusive, so if we matched exactly take the one before. endIndex--; if (endIndex < 0) { endIndex = mSortedStatFiles[intervalType].size() - 1; // 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()); Loading Loading @@ -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()); } Loading
services/usage/java/com/android/server/usage/UsageStatsService.java +1 −3 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -178,7 +176,7 @@ public class UsageStatsService extends SystemService implements } /** * Called by the Bunder stub * Called by the Binder stub */ void shutdown() { synchronized (mLock) { Loading
services/usage/java/com/android/server/usage/UserUsageStatsService.java +119 −94 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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; Loading @@ -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; Loading @@ -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); Loading @@ -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++; } } Loading @@ -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. Loading Loading @@ -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) { final IntervalStats currentStats = mCurrentStats[intervalType]; if (DEBUG) { Slog.d(TAG, mLogPrefix + "SELECT * FROM " + intervalType + " WHERE beginTime >= " + beginTime + " AND endTime < " + endTime); } if (beginTime >= currentStats.endTime) { if (DEBUG) { Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is " + mCurrentStats[bucketType].endTime); + 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); } else if (beginTime >= mCurrentStats[bucketType].beginTime) { // Get the stats from disk. List<T> results = mDatabase.queryUsageStats(intervalType, beginTime, truncatedEndTime, combiner); 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; Slog.d(TAG, "Got " + (results != null ? results.size() : 0) + " results from disk"); Slog.d(TAG, "Current stats beginTime=" + currentStats.beginTime + " endTime=" + currentStats.endTime); } // 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(); // 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 + "SELECT * FROM " + bucketType + " WHERE beginTime >= " + beginTime + " AND endTime < " + endTime); Slog.d(TAG, mLogPrefix + "Returning in-memory stats"); } if (results == null) { results = new ArrayList<>(); } combiner.combine(currentStats, true, results); } final List<T> results = mDatabase.queryUsageStats(bucketType, beginTime, endTime, combiner); if (DEBUG) { Slog.d(TAG, mLogPrefix + "Results: " + (results == null ? 0 : results.size())); Slog.d(TAG, mLogPrefix + "Results: " + (results != null ? results.size() : 0)); } return results; } Loading @@ -250,46 +275,47 @@ 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; } TimeSparseArray<UsageEvents.Event> events = mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events; if (events == null) { 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; } final int startIndex = events.closestIndexOnOrAfter(beginTime); final int startIndex = stats.events.closestIndexOnOrAfter(beginTime); if (startIndex < 0) { return null; return; } ArraySet<String> names = new ArraySet<>(); ArrayList<UsageEvents.Event> results = new ArrayList<>(); final int size = events.size(); final int size = stats.events.size(); for (int i = startIndex; i < size; i++) { if (events.keyAt(i) >= endTime) { break; if (stats.events.keyAt(i) >= endTime) { return; } final UsageEvents.Event event = events.valueAt(i); final UsageEvents.Event event = stats.events.valueAt(i); names.add(event.mPackage); if (event.mClass != null) { names.add(event.mClass); } results.add(event); accumulatedResult.add(event); } String[] table = names.toArray(new String[names.size()]); Arrays.sort(table); return new UsageEvents(results, table); } }); // TODO(adamlesinski): Query the previous days. if (results == null || results.isEmpty()) { return null; } String[] table = names.toArray(new String[names.size()]); Arrays.sort(table); return new UsageEvents(results, table); } void persistActiveStats() { if (mStatsChanged) { Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk"); Loading Loading @@ -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() + ")"); } Loading