DO NOT MERGE ANYWHERE: UsageStatsService: Fix app idle issue at rollover time

App Idle queries are very frequent and so they only check in memory stats.
However, in memory stats can be missing some entries, especially after a rollover, but also
due to a larger bug fixed in master (too risky to take now).

The fix is to do a deep query (reading older files from disk) and maintain a parallel cache
of stats for app idle. That way the rolling window of data required to serve app idle queries
stays in memory.

Bug:26355386
Change-Id: I6a29bbc25214f6a3c2f24c8c079936e66f99e42e
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index b07b815..d8f26ed 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -59,6 +59,7 @@
     private final Context mContext;
     private final UsageStatsDatabase mDatabase;
     private final IntervalStats[] mCurrentStats;
+    private IntervalStats mAppIdleRollingWindow;
     private boolean mStatsChanged = false;
     private final UnixCalendar mDailyExpiryDate;
     private final StatsUpdatedListener mListener;
@@ -138,6 +139,8 @@
             initializeDefaultsForApps(currentTimeMillis, deviceUsageTime,
                     mDatabase.isFirstUpdate());
         }
+
+        refreshAppIdleRollingWindow(currentTimeMillis);
     }
 
     /**
@@ -171,6 +174,7 @@
         persistActiveStats();
         mDatabase.onTimeChanged(newTime - oldTime);
         loadActiveStats(newTime, /* force= */ true, resetBeginIdleTime);
+        refreshAppIdleRollingWindow(newTime);
     }
 
     void reportEvent(UsageEvents.Event event, long deviceUsageTime) {
@@ -212,6 +216,11 @@
             }
         }
 
+        if (event.mEventType != Event.CONFIGURATION_CHANGE) {
+            mAppIdleRollingWindow.update(event.mPackage, event.mTimeStamp, event.mEventType);
+            mAppIdleRollingWindow.updateBeginIdleTime(event.mPackage, deviceUsageTime);
+        }
+
         notifyStatsChanged();
     }
 
@@ -223,6 +232,7 @@
         for (IntervalStats stats : mCurrentStats) {
             stats.updateBeginIdleTime(packageName, beginIdleTime);
         }
+        mAppIdleRollingWindow.updateBeginIdleTime(packageName, beginIdleTime);
         notifyStatsChanged();
     }
 
@@ -230,6 +240,7 @@
         for (IntervalStats stats : mCurrentStats) {
             stats.updateSystemLastUsedTime(packageName, lastUsedTime);
         }
+        mAppIdleRollingWindow.updateSystemLastUsedTime(packageName, lastUsedTime);
         notifyStatsChanged();
     }
 
@@ -388,9 +399,8 @@
     }
 
     long getBeginIdleTime(String packageName) {
-        final IntervalStats yearly = mCurrentStats[UsageStatsManager.INTERVAL_YEARLY];
         UsageStats packageUsage;
-        if ((packageUsage = yearly.packageStats.get(packageName)) == null) {
+        if ((packageUsage = mAppIdleRollingWindow.packageStats.get(packageName)) == null) {
             return -1;
         } else {
             return packageUsage.getBeginIdleTime();
@@ -398,9 +408,8 @@
     }
 
     long getSystemLastUsedTime(String packageName) {
-        final IntervalStats yearly = mCurrentStats[UsageStatsManager.INTERVAL_YEARLY];
         UsageStats packageUsage;
-        if ((packageUsage = yearly.packageStats.get(packageName)) == null) {
+        if ((packageUsage = mAppIdleRollingWindow.packageStats.get(packageName)) == null) {
             return -1;
         } else {
             return packageUsage.getLastTimeSystemUsed();
@@ -462,6 +471,8 @@
         }
         persistActiveStats();
 
+        refreshAppIdleRollingWindow(currentTimeMillis);
+
         final long totalTime = SystemClock.elapsedRealtime() - startTime;
         Slog.i(TAG, mLogPrefix + "Rolling over usage stats complete. Took " + totalTime
                 + " milliseconds");
@@ -521,6 +532,7 @@
                 }
             }
         }
+
         mStatsChanged = false;
         mDailyExpiryDate.setTimeInMillis(currentTimeMillis);
         mDailyExpiryDate.addDays(1);
@@ -530,6 +542,68 @@
                 tempCal.getTimeInMillis() + ")");
     }
 
+    private static void mergePackageStats(IntervalStats dst, IntervalStats src) {
+        dst.endTime = Math.max(dst.endTime, src.endTime);
+
+        final int srcPackageCount = src.packageStats.size();
+        for (int i = 0; i < srcPackageCount; i++) {
+            final String packageName = src.packageStats.keyAt(i);
+            final UsageStats srcStats = src.packageStats.valueAt(i);
+            final UsageStats dstStats = dst.packageStats.get(packageName);
+            if (dstStats == null) {
+                dst.packageStats.put(packageName, new UsageStats(srcStats));
+            } else {
+                dstStats.add(src.packageStats.valueAt(i));
+            }
+        }
+    }
+
+    /**
+     * Merges all the stats into the first element of the resulting list.
+     */
+    private static final StatCombiner<IntervalStats> sPackageStatsMerger =
+            new StatCombiner<IntervalStats>() {
+        @Override
+        public void combine(IntervalStats stats, boolean mutable,
+                            List<IntervalStats> accumulatedResult) {
+            IntervalStats accum;
+            if (accumulatedResult.isEmpty()) {
+                accum = new IntervalStats();
+                accum.beginTime = stats.beginTime;
+                accumulatedResult.add(accum);
+            } else {
+                accum = accumulatedResult.get(0);
+            }
+
+            mergePackageStats(accum, stats);
+        }
+    };
+
+    /**
+     * App idle operates on a rolling window of time. When we roll over time, we end up with a
+     * period of time where in-memory stats are empty and we don't hit the disk for older stats
+     * for performance reasons. Suddenly all apps will become idle.
+     *
+     * Instead, at times we do a deep query to find all the apps that have run in the past few
+     * days and keep the cached data up to date.
+     *
+     * @param currentTimeMillis
+     */
+    void refreshAppIdleRollingWindow(long currentTimeMillis) {
+        // Start the rolling window for AppIdle requests.
+        List<IntervalStats> stats = mDatabase.queryUsageStats(UsageStatsManager.INTERVAL_DAILY,
+                currentTimeMillis - (1000 * 60 * 60 * 24 * 2), currentTimeMillis,
+                sPackageStatsMerger);
+
+        if (stats == null || stats.isEmpty()) {
+            mAppIdleRollingWindow = new IntervalStats();
+            mergePackageStats(mAppIdleRollingWindow,
+                    mCurrentStats[UsageStatsManager.INTERVAL_YEARLY]);
+        } else {
+            mAppIdleRollingWindow = stats.get(0);
+        }
+    }
+
     //
     // -- DUMP related methods --
     //
@@ -552,6 +626,9 @@
             pw.println(" stats");
             printIntervalStats(pw, mCurrentStats[interval], screenOnTime, true);
         }
+
+        pw.println("AppIdleRollingWindow cache");
+        printIntervalStats(pw, mAppIdleRollingWindow, screenOnTime, true);
     }
 
     private String formatDateTime(long dateTime, boolean pretty) {