diff options
author | Adam Lesinski <adamlesinski@google.com> | 2014-07-23 17:34:34 -0700 |
---|---|---|
committer | Adam Lesinski <adamlesinski@google.com> | 2014-07-25 17:18:54 +0000 |
commit | 3c153519ca5f2b66b88901374383f943c9d77df7 (patch) | |
tree | a8af9d9bec018b08cb47e7c6ac622e449c8585d1 /services/usage/java/com | |
parent | f7496d7e0c475e1b3f16129c42c8540bd810ec85 (diff) | |
download | frameworks_base-3c153519ca5f2b66b88901374383f943c9d77df7.zip frameworks_base-3c153519ca5f2b66b88901374383f943c9d77df7.tar.gz frameworks_base-3c153519ca5f2b66b88901374383f943c9d77df7.tar.bz2 |
Add Per-User logging of UsageStats
Change-Id: I4518c5d3c56b3821292accb886f9fb21f3a8b25f
Diffstat (limited to 'services/usage/java/com')
-rw-r--r-- | services/usage/java/com/android/server/usage/UsageStatsService.java | 359 | ||||
-rw-r--r-- | services/usage/java/com/android/server/usage/UserUsageStatsService.java | 275 |
2 files changed, 401 insertions, 233 deletions
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 4018def..1c20d5d 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -19,52 +19,56 @@ package com.android.server.usage; import android.Manifest; import android.app.AppOpsManager; import android.app.usage.IUsageStatsManager; -import android.app.usage.PackageUsageStats; -import android.app.usage.TimeSparseArray; import android.app.usage.UsageStats; import android.app.usage.UsageStatsManager; import android.app.usage.UsageStatsManagerInternal; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.content.pm.UserInfo; import android.os.Binder; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.UserHandle; +import android.os.UserManager; import android.util.ArraySet; import android.util.Slog; +import android.util.SparseArray; + import com.android.internal.os.BackgroundThread; import com.android.server.SystemService; import java.io.File; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Calendar; +import java.util.Arrays; +import java.util.List; -public class UsageStatsService extends SystemService { +public class UsageStatsService extends SystemService implements + UserUsageStatsService.StatsUpdatedListener { static final String TAG = "UsageStatsService"; 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 FLUSH_INTERVAL = DEBUG ? TEN_SECONDS : TWENTY_MINUTES; - private static final int USAGE_STAT_RESULT_LIMIT = 10; - private static final SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + static final int USAGE_STAT_RESULT_LIMIT = 10; // Handler message types. static final int MSG_REPORT_EVENT = 0; static final int MSG_FLUSH_TO_DISK = 1; + static final int MSG_REMOVE_USER = 2; - final Object mLock = new Object(); + private final Object mLock = new Object(); Handler mHandler; AppOpsManager mAppOps; + UserManager mUserManager; - private UsageStatsDatabase mDatabase; - private UsageStats[] mCurrentStats = new UsageStats[UsageStatsManager.BUCKET_COUNT]; - private TimeSparseArray<UsageStats.Event> mCurrentEvents = new TimeSparseArray<>(); - private boolean mStatsChanged = false; - private Calendar mDailyExpiryDate; + private final SparseArray<UserUsageStatsService> mUserState = new SparseArray<>(); + private File mUsageStatsDir; public UsageStatsService(Context context) { super(context); @@ -72,115 +76,96 @@ public class UsageStatsService extends SystemService { @Override public void onStart() { - mDailyExpiryDate = Calendar.getInstance(); mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE); + mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE); mHandler = new H(BackgroundThread.get().getLooper()); File systemDataDir = new File(Environment.getDataDirectory(), "system"); - mDatabase = new UsageStatsDatabase(new File(systemDataDir, "usagestats")); - mDatabase.init(); + mUsageStatsDir = new File(systemDataDir, "usagestats"); + mUsageStatsDir.mkdirs(); + if (!mUsageStatsDir.exists()) { + throw new IllegalStateException("Usage stats directory does not exist: " + + mUsageStatsDir.getAbsolutePath()); + } + + getContext().registerReceiver(new UserRemovedReceiver(), + new IntentFilter(Intent.ACTION_USER_REMOVED)); synchronized (mLock) { - initLocked(); + cleanUpRemovedUsersLocked(); } publishLocalService(UsageStatsManagerInternal.class, new LocalService()); publishBinderService(Context.USAGE_STATS_SERVICE, new BinderService()); } - private void initLocked() { - int nullCount = 0; - for (int i = 0; i < mCurrentStats.length; i++) { - mCurrentStats[i] = mDatabase.getLatestUsageStats(i); - if (mCurrentStats[i] == null) { - nullCount++; + private class UserRemovedReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null && intent.getAction().equals(Intent.ACTION_USER_REMOVED)) { + final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + if (userId >= 0) { + mHandler.obtainMessage(MSG_REMOVE_USER, userId, 0).sendToTarget(); + } } } + } - if (nullCount > 0) { - if (nullCount != mCurrentStats.length) { - // This is weird, but we shouldn't fail if something like this - // happens. - Slog.w(TAG, "Some stats have no latest available"); - } else { - // This must be first boot. - } + @Override + public void onStatsUpdated() { + mHandler.sendEmptyMessageDelayed(MSG_FLUSH_TO_DISK, FLUSH_INTERVAL); + } - // By calling loadActiveStatsLocked, we will - // generate new stats for each bucket. - loadActiveStatsLocked(); - } else { - // Set up the expiry date to be one day from the latest daily stat. - // This may actually be today and we will rollover on the first event - // that is reported. - mDailyExpiryDate.setTimeInMillis( - mCurrentStats[UsageStatsManager.DAILY_BUCKET].mBeginTimeStamp); - mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1); - UsageStatsUtils.truncateDateTo(UsageStatsManager.DAILY_BUCKET, mDailyExpiryDate); - Slog.i(TAG, "Rollover scheduled for " + sDateFormat.format(mDailyExpiryDate.getTime())); + private void cleanUpRemovedUsersLocked() { + final List<UserInfo> users = mUserManager.getUsers(true); + if (users == null || users.size() == 0) { + throw new IllegalStateException("There can't be no users"); } - // Now close off any events that were open at the time this was saved. - for (UsageStats stat : mCurrentStats) { - final int pkgCount = stat.getPackageCount(); - for (int i = 0; i < pkgCount; i++) { - PackageUsageStats pkgStats = stat.getPackage(i); - if (pkgStats.mLastEvent == UsageStats.Event.MOVE_TO_FOREGROUND || - pkgStats.mLastEvent == UsageStats.Event.CONTINUE_PREVIOUS_DAY) { - updateStatsLocked(stat, pkgStats.mPackageName, stat.mLastTimeSaved, - UsageStats.Event.END_OF_DAY); - notifyStatsChangedLocked(); - } - } + ArraySet<String> toDelete = new ArraySet<>(); + String[] fileNames = mUsageStatsDir.list(); + if (fileNames == null) { + // No users to delete. + return; } - } - private void rolloverStatsLocked() { - final long startTime = System.currentTimeMillis(); - Slog.i(TAG, "Rolling over usage stats"); - - // Finish any ongoing events with an END_OF_DAY event. Make a note of which components - // need a new CONTINUE_PREVIOUS_DAY entry. - ArraySet<String> continuePreviousDay = new ArraySet<>(); - for (UsageStats stat : mCurrentStats) { - final int pkgCount = stat.getPackageCount(); - for (int i = 0; i < pkgCount; i++) { - PackageUsageStats pkgStats = stat.getPackage(i); - if (pkgStats.mLastEvent == UsageStats.Event.MOVE_TO_FOREGROUND || - pkgStats.mLastEvent == UsageStats.Event.CONTINUE_PREVIOUS_DAY) { - continuePreviousDay.add(pkgStats.mPackageName); - updateStatsLocked(stat, pkgStats.mPackageName, - mDailyExpiryDate.getTimeInMillis() - 1, UsageStats.Event.END_OF_DAY); - mStatsChanged = true; - } - } + toDelete.addAll(Arrays.asList(fileNames)); + + final int userCount = users.size(); + for (int i = 0; i < userCount; i++) { + final UserInfo userInfo = users.get(i); + toDelete.remove(Integer.toString(userInfo.id)); } - persistActiveStatsLocked(); - mDatabase.prune(); - loadActiveStatsLocked(); - - final int continueCount = continuePreviousDay.size(); - for (int i = 0; i < continueCount; i++) { - String name = continuePreviousDay.valueAt(i); - for (UsageStats stat : mCurrentStats) { - updateStatsLocked(stat, name, - mCurrentStats[UsageStatsManager.DAILY_BUCKET].mBeginTimeStamp, - UsageStats.Event.CONTINUE_PREVIOUS_DAY); - mStatsChanged = true; + final int deleteCount = toDelete.size(); + for (int i = 0; i < deleteCount; i++) { + deleteRecursively(new File(mUsageStatsDir, toDelete.valueAt(i))); + } + } + + private static void deleteRecursively(File f) { + File[] files = f.listFiles(); + if (files != null) { + for (File subFile : files) { + deleteRecursively(subFile); } } - persistActiveStatsLocked(); - final long totalTime = System.currentTimeMillis() - startTime; - Slog.i(TAG, "Rolling over usage stats complete. Took " + totalTime + " milliseconds"); + if (!f.delete()) { + Slog.e(TAG, "Failed to delete " + f); + } } - private void notifyStatsChangedLocked() { - if (!mStatsChanged) { - mStatsChanged = true; - mHandler.sendEmptyMessageDelayed(MSG_FLUSH_TO_DISK, FLUSH_INTERVAL); + private UserUsageStatsService getUserDataAndInitializeIfNeededLocked(int userId) { + UserUsageStatsService service = mUserState.get(userId); + if (service == null) { + service = new UserUsageStatsService(userId, + new File(mUsageStatsDir, Integer.toString(userId)), this); + service.init(); + mUserState.put(userId, service); } + return service; } /** @@ -189,58 +174,45 @@ public class UsageStatsService extends SystemService { void shutdown() { synchronized (mLock) { mHandler.removeMessages(MSG_REPORT_EVENT); - mHandler.removeMessages(MSG_FLUSH_TO_DISK); - persistActiveStatsLocked(); + flushToDiskLocked(); } } - private static String eventToString(int eventType) { - switch (eventType) { - case UsageStats.Event.NONE: - return "NONE"; - case UsageStats.Event.MOVE_TO_BACKGROUND: - return "MOVE_TO_BACKGROUND"; - case UsageStats.Event.MOVE_TO_FOREGROUND: - return "MOVE_TO_FOREGROUND"; - case UsageStats.Event.END_OF_DAY: - return "END_OF_DAY"; - case UsageStats.Event.CONTINUE_PREVIOUS_DAY: - return "CONTINUE_PREVIOUS_DAY"; - default: - return "UNKNOWN"; + /** + * Called by the Binder stub. + */ + void reportEvent(UsageStats.Event event, int userId) { + synchronized (mLock) { + final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId); + service.reportEvent(event); } } /** * Called by the Binder stub. */ - void reportEvent(UsageStats.Event event) { + void flushToDisk() { synchronized (mLock) { - if (DEBUG) { - Slog.d(TAG, "Got usage event for " + event.packageName - + "[" + event.timeStamp + "]: " - + eventToString(event.eventType)); - } - - if (event.timeStamp >= mDailyExpiryDate.getTimeInMillis()) { - // Need to rollover - rolloverStatsLocked(); - } - - mCurrentEvents.append(event.timeStamp, event); + flushToDiskLocked(); + } + } - for (UsageStats stats : mCurrentStats) { - updateStatsLocked(stats, event.packageName, event.timeStamp, event.eventType); - } - notifyStatsChangedLocked(); + /** + * Called by the Binder stub. + */ + void removeUser(int userId) { + synchronized (mLock) { + Slog.i(TAG, "Removing user " + userId + " and all data."); + mUserState.remove(userId); + cleanUpRemovedUsersLocked(); } } /** * Called by the Binder stub. */ - UsageStats[] getUsageStats(int bucketType, long beginTime) { - if (bucketType < 0 || bucketType >= mCurrentStats.length) { + UsageStats[] getUsageStats(int userId, int bucketType, long beginTime) { + if (bucketType < 0 || bucketType >= UsageStatsManager.BUCKET_COUNT) { return UsageStats.EMPTY_STATS; } @@ -250,110 +222,26 @@ public class UsageStatsService extends SystemService { } synchronized (mLock) { - if (beginTime >= mCurrentStats[bucketType].mEndTimeStamp) { - if (DEBUG) { - Slog.d(TAG, "Requesting stats after " + beginTime + " but latest is " - + mCurrentStats[bucketType].mEndTimeStamp); - } - // Nothing newer available. - return UsageStats.EMPTY_STATS; - } else if (beginTime >= mCurrentStats[bucketType].mBeginTimeStamp) { - if (DEBUG) { - Slog.d(TAG, "Returning in-memory stats"); - } - // Fast path for retrieving in-memory state. - // TODO(adamlesinski): This copy just to parcel the object is wasteful. - // It would be nice to parcel it here and send that back, but the Binder API - // would need to change. - return new UsageStats[] { new UsageStats(mCurrentStats[bucketType]) }; - } else { - // Flush any changes that were made to disk before we do a disk query. - persistActiveStatsLocked(); - } + UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId); + return service.getUsageStats(bucketType, beginTime); } - - if (DEBUG) { - Slog.d(TAG, "SELECT * FROM " + bucketType + " WHERE beginTime >= " - + beginTime + " LIMIT " + USAGE_STAT_RESULT_LIMIT); - } - - UsageStats[] results = mDatabase.getUsageStats(bucketType, beginTime, - USAGE_STAT_RESULT_LIMIT); - - if (DEBUG) { - Slog.d(TAG, "Results: " + results.length); - } - return results; } /** * Called by the Binder stub. */ - UsageStats.Event[] getEvents(long time) { + UsageStats.Event[] getEvents(int userId, long time) { return UsageStats.Event.EMPTY_EVENTS; } - private void loadActiveStatsLocked() { - final long timeNow = System.currentTimeMillis(); - - Calendar tempCal = mDailyExpiryDate; - for (int i = 0; i < mCurrentStats.length; i++) { - tempCal.setTimeInMillis(timeNow); - UsageStatsUtils.truncateDateTo(i, tempCal); - - if (mCurrentStats[i] != null && - mCurrentStats[i].mBeginTimeStamp == tempCal.getTimeInMillis()) { - // These are the same, no need to load them (in memory stats are always newer - // than persisted stats). - continue; - } - - UsageStats[] stats = mDatabase.getUsageStats(i, timeNow, 1); - if (stats != null && stats.length > 0) { - mCurrentStats[i] = stats[stats.length - 1]; - } else { - mCurrentStats[i] = UsageStats.create(tempCal.getTimeInMillis(), timeNow); - } + private void flushToDiskLocked() { + final int userCount = mUserState.size(); + for (int i = 0; i < userCount; i++) { + UserUsageStatsService service = mUserState.valueAt(i); + service.persistActiveStats(); } - mStatsChanged = false; - mDailyExpiryDate.setTimeInMillis(timeNow); - mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1); - UsageStatsUtils.truncateDateTo(UsageStatsManager.DAILY_BUCKET, mDailyExpiryDate); - Slog.i(TAG, "Rollover scheduled for " + sDateFormat.format(mDailyExpiryDate.getTime())); - } - - private void persistActiveStatsLocked() { - if (mStatsChanged) { - Slog.i(TAG, "Flushing usage stats to disk"); - try { - for (int i = 0; i < mCurrentStats.length; i++) { - mDatabase.putUsageStats(i, mCurrentStats[i]); - } - mStatsChanged = false; - mHandler.removeMessages(MSG_FLUSH_TO_DISK); - } catch (IOException e) { - Slog.e(TAG, "Failed to persist active stats", e); - } - } - } - - private void updateStatsLocked(UsageStats stats, String packageName, long timeStamp, - int eventType) { - PackageUsageStats pkgStats = stats.getOrCreatePackageUsageStats(packageName); - - // TODO(adamlesinski): Ensure that we recover from incorrect event sequences - // like double MOVE_TO_BACKGROUND, etc. - if (eventType == UsageStats.Event.MOVE_TO_BACKGROUND || - eventType == UsageStats.Event.END_OF_DAY) { - if (pkgStats.mLastEvent == UsageStats.Event.MOVE_TO_FOREGROUND || - pkgStats.mLastEvent == UsageStats.Event.CONTINUE_PREVIOUS_DAY) { - pkgStats.mTotalTimeSpent += timeStamp - pkgStats.mLastTimeUsed; - } - } - pkgStats.mLastEvent = eventType; - pkgStats.mLastTimeUsed = timeStamp; - stats.mEndTimeStamp = timeStamp; + mHandler.removeMessages(MSG_FLUSH_TO_DISK); } class H extends Handler { @@ -365,13 +253,15 @@ public class UsageStatsService extends SystemService { public void handleMessage(Message msg) { switch (msg.what) { case MSG_REPORT_EVENT: - reportEvent((UsageStats.Event) msg.obj); + reportEvent((UsageStats.Event) msg.obj, msg.arg1); break; case MSG_FLUSH_TO_DISK: - synchronized (mLock) { - persistActiveStatsLocked(); - } + flushToDisk(); + break; + + case MSG_REMOVE_USER: + removeUser(msg.arg1); break; default: @@ -401,9 +291,10 @@ public class UsageStatsService extends SystemService { return UsageStats.EMPTY_STATS; } - long token = Binder.clearCallingIdentity(); + final int userId = UserHandle.getCallingUserId(); + final long token = Binder.clearCallingIdentity(); try { - return getUsageStats(bucketType, time); + return getUsageStats(userId, bucketType, time); } finally { Binder.restoreCallingIdentity(token); } @@ -415,9 +306,10 @@ public class UsageStatsService extends SystemService { return UsageStats.Event.EMPTY_EVENTS; } - long token = Binder.clearCallingIdentity(); + final int userId = UserHandle.getCallingUserId(); + final long token = Binder.clearCallingIdentity(); try { - return getEvents(time); + return getEvents(userId, time); } finally { Binder.restoreCallingIdentity(token); } @@ -432,10 +324,11 @@ public class UsageStatsService extends SystemService { private class LocalService extends UsageStatsManagerInternal { @Override - public void reportEvent(ComponentName component, long timeStamp, int eventType) { + public void reportEvent(ComponentName component, int userId, + long timeStamp, int eventType) { UsageStats.Event event = new UsageStats.Event(component.getPackageName(), timeStamp, eventType); - mHandler.obtainMessage(MSG_REPORT_EVENT, event).sendToTarget(); + mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget(); } @Override diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java new file mode 100644 index 0000000..d124188 --- /dev/null +++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java @@ -0,0 +1,275 @@ +package com.android.server.usage; + +import android.app.usage.PackageUsageStats; +import android.app.usage.UsageStats; +import android.app.usage.UsageStatsManager; +import android.util.ArraySet; +import android.util.Slog; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +/** + * A per-user UsageStatsService. All methods are meant to be called with the main lock held + * in UsageStatsService. + */ +class UserUsageStatsService { + private static final String TAG = "UsageStatsService"; + private static final boolean DEBUG = UsageStatsService.DEBUG; + private static final SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + private final UsageStatsDatabase mDatabase; + private final UsageStats[] mCurrentStats = new UsageStats[UsageStatsManager.BUCKET_COUNT]; + private boolean mStatsChanged = false; + private final Calendar mDailyExpiryDate; + private final StatsUpdatedListener mListener; + private final String mLogPrefix; + + interface StatsUpdatedListener { + void onStatsUpdated(); + } + + UserUsageStatsService(int userId, File usageStatsDir, StatsUpdatedListener listener) { + mDailyExpiryDate = Calendar.getInstance(); + mDatabase = new UsageStatsDatabase(usageStatsDir); + mListener = listener; + mLogPrefix = "User[" + Integer.toString(userId) + "] "; + } + + void init() { + mDatabase.init(); + + int nullCount = 0; + for (int i = 0; i < mCurrentStats.length; i++) { + mCurrentStats[i] = mDatabase.getLatestUsageStats(i); + if (mCurrentStats[i] == null) { + nullCount++; + } + } + + if (nullCount > 0) { + if (nullCount != mCurrentStats.length) { + // This is weird, but we shouldn't fail if something like this + // happens. + Slog.w(TAG, mLogPrefix + "Some stats have no latest available"); + } else { + // This must be first boot. + } + + // By calling loadActiveStats, we will + // generate new stats for each bucket. + loadActiveStats(); + } else { + // Set up the expiry date to be one day from the latest daily stat. + // This may actually be today and we will rollover on the first event + // that is reported. + mDailyExpiryDate.setTimeInMillis( + mCurrentStats[UsageStatsManager.DAILY_BUCKET].mBeginTimeStamp); + mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1); + UsageStatsUtils.truncateDateTo(UsageStatsManager.DAILY_BUCKET, mDailyExpiryDate); + Slog.i(TAG, mLogPrefix + "Rollover scheduled for " + + sDateFormat.format(mDailyExpiryDate.getTime())); + } + + // Now close off any events that were open at the time this was saved. + for (UsageStats stat : mCurrentStats) { + final int pkgCount = stat.getPackageCount(); + for (int i = 0; i < pkgCount; i++) { + PackageUsageStats pkgStats = stat.getPackage(i); + if (pkgStats.mLastEvent == UsageStats.Event.MOVE_TO_FOREGROUND || + pkgStats.mLastEvent == UsageStats.Event.CONTINUE_PREVIOUS_DAY) { + updateStats(stat, pkgStats.mPackageName, stat.mLastTimeSaved, + UsageStats.Event.END_OF_DAY); + notifyStatsChanged(); + } + } + } + } + + void reportEvent(UsageStats.Event event) { + if (DEBUG) { + Slog.d(TAG, mLogPrefix + "Got usage event for " + event.packageName + + "[" + event.timeStamp + "]: " + + eventToString(event.eventType)); + } + + if (event.timeStamp >= mDailyExpiryDate.getTimeInMillis()) { + // Need to rollover + rolloverStats(); + } + + for (UsageStats stats : mCurrentStats) { + updateStats(stats, event.packageName, event.timeStamp, event.eventType); + } + + notifyStatsChanged(); + } + + UsageStats[] getUsageStats(int bucketType, long beginTime) { + if (beginTime >= mCurrentStats[bucketType].mEndTimeStamp) { + if (DEBUG) { + Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is " + + mCurrentStats[bucketType].mEndTimeStamp); + } + // Nothing newer available. + return UsageStats.EMPTY_STATS; + + } else if (beginTime >= mCurrentStats[bucketType].mBeginTimeStamp) { + if (DEBUG) { + Slog.d(TAG, mLogPrefix + "Returning in-memory stats"); + } + // Fast path for retrieving in-memory state. + // TODO(adamlesinski): This copy just to parcel the object is wasteful. + // It would be nice to parcel it here and send that back, but the Binder API + // would need to change. + return new UsageStats[] { new UsageStats(mCurrentStats[bucketType]) }; + + } else { + // Flush any changes that were made to disk before we do a disk query. + persistActiveStats(); + } + + if (DEBUG) { + Slog.d(TAG, mLogPrefix + "SELECT * FROM " + bucketType + " WHERE beginTime >= " + + beginTime + " LIMIT " + UsageStatsService.USAGE_STAT_RESULT_LIMIT); + } + + final UsageStats[] results = mDatabase.getUsageStats(bucketType, beginTime, + UsageStatsService.USAGE_STAT_RESULT_LIMIT); + + if (DEBUG) { + Slog.d(TAG, mLogPrefix + "Results: " + results.length); + } + return results; + } + + void persistActiveStats() { + if (mStatsChanged) { + Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk"); + try { + for (int i = 0; i < mCurrentStats.length; i++) { + mDatabase.putUsageStats(i, mCurrentStats[i]); + } + mStatsChanged = false; + } catch (IOException e) { + Slog.e(TAG, mLogPrefix + "Failed to persist active stats", e); + } + } + } + + private void rolloverStats() { + final long startTime = System.currentTimeMillis(); + Slog.i(TAG, mLogPrefix + "Rolling over usage stats"); + + // Finish any ongoing events with an END_OF_DAY event. Make a note of which components + // need a new CONTINUE_PREVIOUS_DAY entry. + ArraySet<String> continuePreviousDay = new ArraySet<>(); + for (UsageStats stat : mCurrentStats) { + final int pkgCount = stat.getPackageCount(); + for (int i = 0; i < pkgCount; i++) { + PackageUsageStats pkgStats = stat.getPackage(i); + if (pkgStats.mLastEvent == UsageStats.Event.MOVE_TO_FOREGROUND || + pkgStats.mLastEvent == UsageStats.Event.CONTINUE_PREVIOUS_DAY) { + continuePreviousDay.add(pkgStats.mPackageName); + updateStats(stat, pkgStats.mPackageName, + mDailyExpiryDate.getTimeInMillis() - 1, UsageStats.Event.END_OF_DAY); + mStatsChanged = true; + } + } + } + + persistActiveStats(); + mDatabase.prune(); + loadActiveStats(); + + final int continueCount = continuePreviousDay.size(); + for (int i = 0; i < continueCount; i++) { + String name = continuePreviousDay.valueAt(i); + for (UsageStats stat : mCurrentStats) { + updateStats(stat, name, + mCurrentStats[UsageStatsManager.DAILY_BUCKET].mBeginTimeStamp, + UsageStats.Event.CONTINUE_PREVIOUS_DAY); + mStatsChanged = true; + } + } + persistActiveStats(); + + final long totalTime = System.currentTimeMillis() - startTime; + Slog.i(TAG, mLogPrefix + "Rolling over usage stats complete. Took " + totalTime + + " milliseconds"); + } + + private void notifyStatsChanged() { + if (!mStatsChanged) { + mStatsChanged = true; + mListener.onStatsUpdated(); + } + } + + private void loadActiveStats() { + final long timeNow = System.currentTimeMillis(); + + Calendar tempCal = mDailyExpiryDate; + for (int i = 0; i < mCurrentStats.length; i++) { + tempCal.setTimeInMillis(timeNow); + UsageStatsUtils.truncateDateTo(i, tempCal); + + if (mCurrentStats[i] != null && + mCurrentStats[i].mBeginTimeStamp == tempCal.getTimeInMillis()) { + // These are the same, no need to load them (in memory stats are always newer + // than persisted stats). + continue; + } + + UsageStats[] stats = mDatabase.getUsageStats(i, timeNow, 1); + if (stats != null && stats.length > 0) { + mCurrentStats[i] = stats[stats.length - 1]; + } else { + mCurrentStats[i] = UsageStats.create(tempCal.getTimeInMillis(), timeNow); + } + } + mStatsChanged = false; + mDailyExpiryDate.setTimeInMillis(timeNow); + mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1); + UsageStatsUtils.truncateDateTo(UsageStatsManager.DAILY_BUCKET, mDailyExpiryDate); + Slog.i(TAG, mLogPrefix + "Rollover scheduled for " + + sDateFormat.format(mDailyExpiryDate.getTime())); + } + + private void updateStats(UsageStats stats, String packageName, long timeStamp, + int eventType) { + PackageUsageStats pkgStats = stats.getOrCreatePackageUsageStats(packageName); + + // TODO(adamlesinski): Ensure that we recover from incorrect event sequences + // like double MOVE_TO_BACKGROUND, etc. + if (eventType == UsageStats.Event.MOVE_TO_BACKGROUND || + eventType == UsageStats.Event.END_OF_DAY) { + if (pkgStats.mLastEvent == UsageStats.Event.MOVE_TO_FOREGROUND || + pkgStats.mLastEvent == UsageStats.Event.CONTINUE_PREVIOUS_DAY) { + pkgStats.mTotalTimeSpent += timeStamp - pkgStats.mLastTimeUsed; + } + } + pkgStats.mLastEvent = eventType; + pkgStats.mLastTimeUsed = timeStamp; + stats.mEndTimeStamp = timeStamp; + } + + private static String eventToString(int eventType) { + switch (eventType) { + case UsageStats.Event.NONE: + return "NONE"; + case UsageStats.Event.MOVE_TO_BACKGROUND: + return "MOVE_TO_BACKGROUND"; + case UsageStats.Event.MOVE_TO_FOREGROUND: + return "MOVE_TO_FOREGROUND"; + case UsageStats.Event.END_OF_DAY: + return "END_OF_DAY"; + case UsageStats.Event.CONTINUE_PREVIOUS_DAY: + return "CONTINUE_PREVIOUS_DAY"; + default: + return "UNKNOWN"; + } + } +} |