summaryrefslogtreecommitdiffstats
path: root/services/usage/java/com/android/server
diff options
context:
space:
mode:
authorAdam Lesinski <adamlesinski@google.com>2014-07-23 17:34:34 -0700
committerAdam Lesinski <adamlesinski@google.com>2014-07-25 17:18:54 +0000
commit3c153519ca5f2b66b88901374383f943c9d77df7 (patch)
treea8af9d9bec018b08cb47e7c6ac622e449c8585d1 /services/usage/java/com/android/server
parentf7496d7e0c475e1b3f16129c42c8540bd810ec85 (diff)
downloadframeworks_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/android/server')
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsService.java359
-rw-r--r--services/usage/java/com/android/server/usage/UserUsageStatsService.java275
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";
+ }
+ }
+}