Returns UsageEvents from previous days

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

Bug: 16951313

Change-Id: I2473b8ef8dc1e2f6be22d4c689b96e346bdcafd5
diff --git a/services/usage/java/com/android/server/usage/UnixCalendar.java b/services/usage/java/com/android/server/usage/UnixCalendar.java
new file mode 100644
index 0000000..ce06a91
--- /dev/null
+++ b/services/usage/java/com/android/server/usage/UnixCalendar.java
@@ -0,0 +1,99 @@
+/**
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package com.android.server.usage;
+
+import android.app.usage.UsageStatsManager;
+
+/**
+ * A handy calendar object that knows nothing of Locale's or TimeZones. This simplifies
+ * interval book-keeping. It is *NOT* meant to be used as a user-facing calendar, as it has
+ * no concept of Locale or TimeZone.
+ */
+public class UnixCalendar {
+    private static final long DAY_IN_MILLIS = 24 * 60 * 60 * 1000;
+    private static final long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
+    private static final long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
+    private static final long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
+    private long mTime;
+
+    public UnixCalendar(long time) {
+        mTime = time;
+    }
+
+    public void truncateToDay() {
+        mTime -= mTime % DAY_IN_MILLIS;
+    }
+
+    public void truncateToWeek() {
+        mTime -= mTime % WEEK_IN_MILLIS;
+    }
+
+    public void truncateToMonth() {
+        mTime -= mTime % MONTH_IN_MILLIS;
+    }
+
+    public void truncateToYear() {
+        mTime -= mTime % YEAR_IN_MILLIS;
+    }
+
+    public void addDays(int val) {
+        mTime += val * DAY_IN_MILLIS;
+    }
+
+    public void addWeeks(int val) {
+        mTime += val * WEEK_IN_MILLIS;
+    }
+
+    public void addMonths(int val) {
+        mTime += val * MONTH_IN_MILLIS;
+    }
+
+    public void addYears(int val) {
+        mTime += val * YEAR_IN_MILLIS;
+    }
+
+    public void setTimeInMillis(long time) {
+        mTime = time;
+    }
+
+    public long getTimeInMillis() {
+        return mTime;
+    }
+
+    public static void truncateTo(UnixCalendar calendar, int intervalType) {
+        switch (intervalType) {
+            case UsageStatsManager.INTERVAL_YEARLY:
+                calendar.truncateToYear();
+                break;
+
+            case UsageStatsManager.INTERVAL_MONTHLY:
+                calendar.truncateToMonth();
+                break;
+
+            case UsageStatsManager.INTERVAL_WEEKLY:
+                calendar.truncateToWeek();
+                break;
+
+            case UsageStatsManager.INTERVAL_DAILY:
+                calendar.truncateToDay();
+                break;
+
+            default:
+                throw new UnsupportedOperationException("Can't truncate date to interval " +
+                        intervalType);
+        }
+    }
+}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
index 37340a4..6c5fa78 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
@@ -21,24 +21,30 @@
 import android.util.AtomicFile;
 import android.util.Slog;
 
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
 import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
 import java.io.FilenameFilter;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Calendar;
 import java.util.List;
 
 /**
  * Provides an interface to query for UsageStat data from an XML database.
  */
 class UsageStatsDatabase {
+    private static final int CURRENT_VERSION = 1;
+
     private static final String TAG = "UsageStatsDatabase";
     private static final boolean DEBUG = UsageStatsService.DEBUG;
 
     private final Object mLock = new Object();
     private final File[] mIntervalDirs;
     private final TimeSparseArray<AtomicFile>[] mSortedStatFiles;
-    private final Calendar mCal;
+    private final UnixCalendar mCal;
+    private final File mVersionFile;
 
     public UsageStatsDatabase(File dir) {
         mIntervalDirs = new File[] {
@@ -47,8 +53,9 @@
                 new File(dir, "monthly"),
                 new File(dir, "yearly"),
         };
+        mVersionFile = new File(dir, "version");
         mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length];
-        mCal = Calendar.getInstance();
+        mCal = new UnixCalendar(0);
     }
 
     /**
@@ -64,6 +71,8 @@
                 }
             }
 
+            checkVersionLocked();
+
             final FilenameFilter backupFileFilter = new FilenameFilter() {
                 @Override
                 public boolean accept(File dir, String name) {
@@ -88,6 +97,43 @@
         }
     }
 
+    private void checkVersionLocked() {
+        int version;
+        try (BufferedReader reader = new BufferedReader(new FileReader(mVersionFile))) {
+            version = Integer.parseInt(reader.readLine());
+        } catch (NumberFormatException | IOException e) {
+            version = 0;
+        }
+
+        if (version != CURRENT_VERSION) {
+            Slog.i(TAG, "Upgrading from version " + version + " to " + CURRENT_VERSION);
+            doUpgradeLocked(version, CURRENT_VERSION);
+
+            try (BufferedWriter writer = new BufferedWriter(new FileWriter(mVersionFile))) {
+                writer.write(Integer.toString(CURRENT_VERSION));
+            } catch (IOException e) {
+                Slog.e(TAG, "Failed to write new version");
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    private void doUpgradeLocked(int thisVersion, int nextVersion) {
+        if (thisVersion == 0) {
+            // Delete all files if we are version 0. This is a pre-release version,
+            // so this is fine.
+            Slog.i(TAG, "Deleting all usage stats files");
+            for (int i = 0; i < mIntervalDirs.length; i++) {
+                File[] files = mIntervalDirs[i].listFiles();
+                if (files != null) {
+                    for (File f : files) {
+                        f.delete();
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Get the latest stats that exist for this interval type.
      */
@@ -161,25 +207,48 @@
                 throw new IllegalArgumentException("Bad interval type " + intervalType);
             }
 
-            if (endTime < beginTime) {
+            final TimeSparseArray<AtomicFile> intervalStats = mSortedStatFiles[intervalType];
+
+            if (endTime <= beginTime) {
+                if (DEBUG) {
+                    Slog.d(TAG, "endTime(" + endTime + ") <= beginTime(" + beginTime + ")");
+                }
                 return null;
             }
 
-            final int startIndex = mSortedStatFiles[intervalType].closestIndexOnOrBefore(beginTime);
+            int startIndex = intervalStats.closestIndexOnOrBefore(beginTime);
             if (startIndex < 0) {
+                // All the stats available have timestamps after beginTime, which means they all
+                // match.
+                startIndex = 0;
+            }
+
+            int endIndex = intervalStats.closestIndexOnOrBefore(endTime);
+            if (endIndex < 0) {
+                // All the stats start after this range ends, so nothing matches.
+                if (DEBUG) {
+                    Slog.d(TAG, "No results for this range. All stats start after.");
+                }
                 return null;
             }
 
-            int endIndex = mSortedStatFiles[intervalType].closestIndexOnOrAfter(endTime);
-            if (endIndex < 0) {
-                endIndex = mSortedStatFiles[intervalType].size() - 1;
+            if (intervalStats.keyAt(endIndex) == endTime) {
+                // The endTime is exclusive, so if we matched exactly take the one before.
+                endIndex--;
+                if (endIndex < 0) {
+                    // All the stats start after this range ends, so nothing matches.
+                    if (DEBUG) {
+                        Slog.d(TAG, "No results for this range. All stats start after.");
+                    }
+                    return null;
+                }
             }
 
             try {
                 IntervalStats stats = new IntervalStats();
                 ArrayList<T> results = new ArrayList<>();
                 for (int i = startIndex; i <= endIndex; i++) {
-                    final AtomicFile f = mSortedStatFiles[intervalType].valueAt(i);
+                    final AtomicFile f = intervalStats.valueAt(i);
 
                     if (DEBUG) {
                         Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath());
@@ -230,22 +299,22 @@
         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());
         }
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index e77bf86..8aa3d66 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -62,9 +62,7 @@
     static final boolean DEBUG = false;
     private static final long TEN_SECONDS = 10 * 1000;
     private static final long TWENTY_MINUTES = 20 * 60 * 1000;
-    private static final long TWO_MINUTES = 2 * 60 * 1000;
     private static final long FLUSH_INTERVAL = DEBUG ? TEN_SECONDS : TWENTY_MINUTES;
-    private static final long END_TIME_DELAY = DEBUG ? 0 : TWO_MINUTES;
 
     // Handler message types.
     static final int MSG_REPORT_EVENT = 0;
@@ -178,7 +176,7 @@
     }
 
     /**
-     * Called by the Bunder stub
+     * Called by the Binder stub
      */
     void shutdown() {
         synchronized (mLock) {
diff --git a/services/usage/java/com/android/server/usage/UsageStatsUtils.java b/services/usage/java/com/android/server/usage/UsageStatsUtils.java
deleted file mode 100644
index dd5f3b9..0000000
--- a/services/usage/java/com/android/server/usage/UsageStatsUtils.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy
- * of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- */
-
-package com.android.server.usage;
-
-import android.app.usage.UsageStatsManager;
-
-import java.util.Calendar;
-
-/**
- * A collection of utility methods used by the UsageStatsService and accompanying classes.
- */
-final class UsageStatsUtils {
-    private UsageStatsUtils() {}
-
-    /**
-     * Truncates the date to the given UsageStats bucket. For example, if the bucket is
-     * {@link UsageStatsManager#INTERVAL_YEARLY}, the date is truncated to the 1st day of the year,
-     * with the time set to 00:00:00.
-     *
-     * @param bucket The UsageStats bucket to truncate to.
-     * @param cal The date to truncate.
-     */
-    public static void truncateDateTo(int bucket, Calendar cal) {
-        cal.set(Calendar.HOUR_OF_DAY, 0);
-        cal.set(Calendar.MINUTE, 0);
-        cal.set(Calendar.SECOND, 0);
-        cal.set(Calendar.MILLISECOND, 0);
-
-        switch (bucket) {
-            case UsageStatsManager.INTERVAL_YEARLY:
-                cal.set(Calendar.DAY_OF_YEAR, 0);
-                break;
-
-            case UsageStatsManager.INTERVAL_MONTHLY:
-                cal.set(Calendar.DAY_OF_MONTH, 0);
-                break;
-
-            case UsageStatsManager.INTERVAL_WEEKLY:
-                cal.set(Calendar.DAY_OF_WEEK, 0);
-                break;
-
-            case UsageStatsManager.INTERVAL_DAILY:
-                break;
-
-            default:
-                throw new UnsupportedOperationException("Can't truncate date to bucket " + bucket);
-        }
-    }
-}
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index 7142a99..8876495 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -32,7 +32,6 @@
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Calendar;
 import java.util.List;
 
 /**
@@ -47,7 +46,7 @@
     private final UsageStatsDatabase mDatabase;
     private final IntervalStats[] mCurrentStats;
     private boolean mStatsChanged = false;
-    private final Calendar mDailyExpiryDate;
+    private final UnixCalendar mDailyExpiryDate;
     private final StatsUpdatedListener mListener;
     private final String mLogPrefix;
 
@@ -56,7 +55,7 @@
     }
 
     UserUsageStatsService(int userId, File usageStatsDir, StatsUpdatedListener listener) {
-        mDailyExpiryDate = Calendar.getInstance();
+        mDailyExpiryDate = new UnixCalendar(0);
         mDatabase = new UsageStatsDatabase(usageStatsDir);
         mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
         mListener = listener;
@@ -66,6 +65,7 @@
     void init() {
         mDatabase.init();
 
+        final long timeNow = System.currentTimeMillis();
         int nullCount = 0;
         for (int i = 0; i < mCurrentStats.length; i++) {
             mCurrentStats[i] = mDatabase.getLatestUsageStats(i);
@@ -73,6 +73,11 @@
                 // Find out how many intervals we don't have data for.
                 // Ideally it should be all or none.
                 nullCount++;
+            } else if (mCurrentStats[i].beginTime > timeNow) {
+                Slog.e(TAG, mLogPrefix + "Interval " + i + " has stat in the future " +
+                        mCurrentStats[i].beginTime);
+                mCurrentStats[i] = null;
+                nullCount++;
             }
         }
 
@@ -94,10 +99,11 @@
             // that is reported.
             mDailyExpiryDate.setTimeInMillis(
                     mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime);
-            mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1);
-            UsageStatsUtils.truncateDateTo(UsageStatsManager.INTERVAL_DAILY, mDailyExpiryDate);
-            Slog.i(TAG, mLogPrefix + "Rollover scheduled for "
-                    + sDateFormat.format(mDailyExpiryDate.getTime()));
+            mDailyExpiryDate.addDays(1);
+            mDailyExpiryDate.truncateToDay();
+            Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
+                    sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) +
+                    "(" + mDailyExpiryDate.getTimeInMillis() + ")");
         }
 
         // Now close off any events that were open at the time this was saved.
@@ -195,49 +201,68 @@
      * and bucket, then calls the {@link com.android.server.usage.UsageStatsDatabase.StatCombiner}
      * provided to select the stats to use from the IntervalStats object.
      */
-    private <T> List<T> queryStats(int bucketType, long beginTime, long endTime,
+    private <T> List<T> queryStats(int intervalType, final long beginTime, final long endTime,
             StatCombiner<T> combiner) {
-        if (bucketType == UsageStatsManager.INTERVAL_BEST) {
-            bucketType = mDatabase.findBestFitBucket(beginTime, endTime);
+        if (intervalType == UsageStatsManager.INTERVAL_BEST) {
+            intervalType = mDatabase.findBestFitBucket(beginTime, endTime);
+            if (intervalType < 0) {
+                // Nothing saved to disk yet, so every stat is just as equal (no rollover has
+                // occurred.
+                intervalType = UsageStatsManager.INTERVAL_DAILY;
+            }
         }
 
-        if (bucketType < 0 || bucketType >= mCurrentStats.length) {
+        if (intervalType < 0 || intervalType >= mCurrentStats.length) {
             if (DEBUG) {
-                Slog.d(TAG, mLogPrefix + "Bad bucketType used " + bucketType);
+                Slog.d(TAG, mLogPrefix + "Bad intervalType used " + intervalType);
             }
             return null;
         }
 
-        if (beginTime >= mCurrentStats[bucketType].endTime) {
-            if (DEBUG) {
-                Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is "
-                        + mCurrentStats[bucketType].endTime);
-            }
-            // Nothing newer available.
-            return null;
-
-        } else if (beginTime >= mCurrentStats[bucketType].beginTime) {
-            if (DEBUG) {
-                Slog.d(TAG, mLogPrefix + "Returning in-memory stats for bucket " + bucketType);
-            }
-            // Fast path for retrieving in-memory state.
-            ArrayList<T> results = new ArrayList<>();
-            combiner.combine(mCurrentStats[bucketType], true, results);
-            return results;
-        }
-
-        // Flush any changes that were made to disk before we do a disk query.
-        // If we're not grabbing the ongoing stats, no need to persist.
-        persistActiveStats();
+        final IntervalStats currentStats = mCurrentStats[intervalType];
 
         if (DEBUG) {
-            Slog.d(TAG, mLogPrefix + "SELECT * FROM " + bucketType + " WHERE beginTime >= "
+            Slog.d(TAG, mLogPrefix + "SELECT * FROM " + intervalType + " WHERE beginTime >= "
                     + beginTime + " AND endTime < " + endTime);
         }
 
-        final List<T> results = mDatabase.queryUsageStats(bucketType, beginTime, endTime, combiner);
+        if (beginTime >= currentStats.endTime) {
+            if (DEBUG) {
+                Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is "
+                        + currentStats.endTime);
+            }
+            // Nothing newer available.
+            return null;
+        }
+
+        // Truncate the endTime to just before the in-memory stats. Then, we'll append the
+        // in-memory stats to the results (if necessary) so as to avoid writing to disk too
+        // often.
+        final long truncatedEndTime = Math.min(currentStats.beginTime, endTime);
+
+        // Get the stats from disk.
+        List<T> results = mDatabase.queryUsageStats(intervalType, beginTime,
+                truncatedEndTime, combiner);
         if (DEBUG) {
-            Slog.d(TAG, mLogPrefix + "Results: " + (results == null ? 0 : results.size()));
+            Slog.d(TAG, "Got " + (results != null ? results.size() : 0) + " results from disk");
+            Slog.d(TAG, "Current stats beginTime=" + currentStats.beginTime +
+                    " endTime=" + currentStats.endTime);
+        }
+
+        // Now check if the in-memory stats match the range and add them if they do.
+        if (beginTime < currentStats.endTime && endTime > currentStats.beginTime) {
+            if (DEBUG) {
+                Slog.d(TAG, mLogPrefix + "Returning in-memory stats");
+            }
+
+            if (results == null) {
+                results = new ArrayList<>();
+            }
+            combiner.combine(currentStats, true, results);
+        }
+
+        if (DEBUG) {
+            Slog.d(TAG, mLogPrefix + "Results: " + (results != null ? results.size() : 0));
         }
         return results;
     }
@@ -250,44 +275,45 @@
         return queryStats(bucketType, beginTime, endTime, sConfigStatsCombiner);
     }
 
-    UsageEvents queryEvents(long beginTime, long endTime) {
-        if (endTime > mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime) {
-            if (beginTime > mCurrentStats[UsageStatsManager.INTERVAL_DAILY].endTime) {
-                return null;
-            }
+    UsageEvents queryEvents(final long beginTime, final long endTime) {
+        final ArraySet<String> names = new ArraySet<>();
+        List<UsageEvents.Event> results = queryStats(UsageStatsManager.INTERVAL_DAILY,
+                beginTime, endTime, new StatCombiner<UsageEvents.Event>() {
+                    @Override
+                    public void combine(IntervalStats stats, boolean mutable,
+                            List<UsageEvents.Event> accumulatedResult) {
+                        if (stats.events == null) {
+                            return;
+                        }
 
-            TimeSparseArray<UsageEvents.Event> events =
-                    mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events;
-            if (events == null) {
-                return null;
-            }
+                        final int startIndex = stats.events.closestIndexOnOrAfter(beginTime);
+                        if (startIndex < 0) {
+                            return;
+                        }
 
-            final int startIndex = events.closestIndexOnOrAfter(beginTime);
-            if (startIndex < 0) {
-                return null;
-            }
+                        final int size = stats.events.size();
+                        for (int i = startIndex; i < size; i++) {
+                            if (stats.events.keyAt(i) >= endTime) {
+                                return;
+                            }
 
-            ArraySet<String> names = new ArraySet<>();
-            ArrayList<UsageEvents.Event> results = new ArrayList<>();
-            final int size = events.size();
-            for (int i = startIndex; i < size; i++) {
-                if (events.keyAt(i) >= endTime) {
-                    break;
-                }
-                final UsageEvents.Event event = events.valueAt(i);
-                names.add(event.mPackage);
-                if (event.mClass != null) {
-                    names.add(event.mClass);
-                }
-                results.add(event);
-            }
-            String[] table = names.toArray(new String[names.size()]);
-            Arrays.sort(table);
-            return new UsageEvents(results, table);
+                            final UsageEvents.Event event = stats.events.valueAt(i);
+                            names.add(event.mPackage);
+                            if (event.mClass != null) {
+                                names.add(event.mClass);
+                            }
+                            accumulatedResult.add(event);
+                        }
+                    }
+                });
+
+        if (results == null || results.isEmpty()) {
+            return null;
         }
 
-        // TODO(adamlesinski): Query the previous days.
-        return null;
+        String[] table = names.toArray(new String[names.size()]);
+        Arrays.sort(table);
+        return new UsageEvents(results, table);
     }
 
     void persistActiveStats() {
@@ -360,54 +386,53 @@
     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);
+                    Slog.d(TAG, mLogPrefix + "Loading existing stats @ " +
+                            sDateFormat.format(lastBeginTime) + "(" + lastBeginTime +
+                            ") for interval " + intervalType);
                 }
-                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");
-                    }
-                }
+                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() + ")");
     }