Add Per-User logging of UsageStats

Change-Id: I4518c5d3c56b3821292accb886f9fb21f3a8b25f
diff --git a/core/java/android/app/usage/UsageStatsManagerInternal.java b/core/java/android/app/usage/UsageStatsManagerInternal.java
index d6345f3..0d41be2 100644
--- a/core/java/android/app/usage/UsageStatsManagerInternal.java
+++ b/core/java/android/app/usage/UsageStatsManagerInternal.java
@@ -29,11 +29,13 @@
      * Reports an event to the UsageStatsManager.
      *
      * @param component The component for which this event ocurred.
+     * @param userId The user id to which the component belongs to.
      * @param timeStamp The time at which this event ocurred.
      * @param eventType The event that occured. Valid values can be found at
      * {@link android.app.usage.UsageStats.Event}
      */
-    public abstract void reportEvent(ComponentName component, long timeStamp, int eventType);
+    public abstract void reportEvent(ComponentName component, int userId,
+            long timeStamp, int eventType);
 
     /**
      * Prepares the UsageStatsService for shutdown.
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index f38b280..3f95ae2 100755
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -3098,7 +3098,8 @@
         final BatteryStatsImpl stats = mBatteryStatsService.getActiveStatistics();
         if (resumed) {
             if (mUsageStatsService != null) {
-                mUsageStatsService.reportEvent(component.realActivity, System.currentTimeMillis(),
+                mUsageStatsService.reportEvent(component.realActivity, component.userId,
+                        System.currentTimeMillis(),
                         UsageStats.Event.MOVE_TO_FOREGROUND);
             }
             synchronized (stats) {
@@ -3106,7 +3107,8 @@
             }
         } else {
             if (mUsageStatsService != null) {
-                mUsageStatsService.reportEvent(component.realActivity, System.currentTimeMillis(),
+                mUsageStatsService.reportEvent(component.realActivity, component.userId,
+                        System.currentTimeMillis(),
                         UsageStats.Event.MOVE_TO_BACKGROUND);
             }
             synchronized (stats) {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 9e67908..0cc6e0f 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -352,6 +352,9 @@
         mFirstBoot = mPackageManagerService.isFirstBoot();
         mPackageManager = mSystemContext.getPackageManager();
 
+        Slog.i(TAG, "User Service");
+        ServiceManager.addService(Context.USER_SERVICE, UserManagerService.getInstance());
+
         // Initialize attribute cache used to cache resources from packages.
         AttributeCache.init(mSystemContext);
 
@@ -434,10 +437,6 @@
             Slog.i(TAG, "Entropy Mixer");
             ServiceManager.addService("entropy", new EntropyMixer(context));
 
-            Slog.i(TAG, "User Service");
-            ServiceManager.addService(Context.USER_SERVICE,
-                    UserManagerService.getInstance());
-
             mContentResolver = context.getContentResolver();
 
             // The AccountManager must come before the ContentService
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 @@
 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 @@
 
     @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 {
 
-        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.
-            }
-
-            // 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()));
-        }
-
-        // 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();
+        @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();
                 }
             }
         }
     }
 
-    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;
-                }
-            }
-        }
-
-        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;
-            }
-        }
-        persistActiveStatsLocked();
-
-        final long totalTime = System.currentTimeMillis() - startTime;
-        Slog.i(TAG, "Rolling over usage stats complete. Took " + totalTime + " milliseconds");
+    @Override
+    public void onStatsUpdated() {
+        mHandler.sendEmptyMessageDelayed(MSG_FLUSH_TO_DISK, FLUSH_INTERVAL);
     }
 
-    private void notifyStatsChangedLocked() {
-        if (!mStatsChanged) {
-            mStatsChanged = true;
-            mHandler.sendEmptyMessageDelayed(MSG_FLUSH_TO_DISK, FLUSH_INTERVAL);
+    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");
         }
+
+        ArraySet<String> toDelete = new ArraySet<>();
+        String[] fileNames = mUsageStatsDir.list();
+        if (fileNames == null) {
+            // No users to delete.
+            return;
+        }
+
+        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));
+        }
+
+        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);
+            }
+        }
+
+        if (!f.delete()) {
+            Slog.e(TAG, "Failed to delete " + f);
+        }
+    }
+
+    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 @@
     void shutdown() {
         synchronized (mLock) {
             mHandler.removeMessages(MSG_REPORT_EVENT);
-            mHandler.removeMessages(MSG_FLUSH_TO_DISK);
-            persistActiveStatsLocked();
-        }
-    }
-
-    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";
+            flushToDiskLocked();
         }
     }
 
     /**
      * Called by the Binder stub.
      */
-    void reportEvent(UsageStats.Event event) {
+    void reportEvent(UsageStats.Event event, int userId) {
         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);
-
-            for (UsageStats stats : mCurrentStats) {
-                updateStatsLocked(stats, event.packageName, event.timeStamp, event.eventType);
-            }
-            notifyStatsChangedLocked();
+            final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId);
+            service.reportEvent(event);
         }
     }
 
     /**
      * Called by the Binder stub.
      */
-    UsageStats[] getUsageStats(int bucketType, long beginTime) {
-        if (bucketType < 0 || bucketType >= mCurrentStats.length) {
+    void flushToDisk() {
+        synchronized (mLock) {
+            flushToDiskLocked();
+        }
+    }
+
+    /**
+     * 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 userId, int bucketType, long beginTime) {
+        if (bucketType < 0 || bucketType >= UsageStatsManager.BUCKET_COUNT) {
             return UsageStats.EMPTY_STATS;
         }
 
@@ -250,110 +222,26 @@
         }
 
         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 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 @@
                 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 @@
                 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 @@
     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";
+        }
+    }
+}