Track app idle history and dump it

"dumpsys usagestats history" will show the
active state of each app for the last 100 hours,
if the device hasn't rebooted.

Bug: 20066058
Change-Id: I703e5bc121298e4363c202da56fffb0b8534bcaf
diff --git a/services/usage/java/com/android/server/usage/AppIdleHistory.java b/services/usage/java/com/android/server/usage/AppIdleHistory.java
new file mode 100644
index 0000000..9d3db16
--- /dev/null
+++ b/services/usage/java/com/android/server/usage/AppIdleHistory.java
@@ -0,0 +1,95 @@
+/**
+ * Copyright (C) 2015 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.util.ArrayMap;
+import android.util.SparseArray;
+
+import com.android.internal.util.IndentingPrintWriter;
+
+/**
+ * Keeps track of recent active state changes in apps.
+ * Access should be guarded by a lock by the caller.
+ */
+public class AppIdleHistory {
+
+    private SparseArray<ArrayMap<String,byte[]>> idleHistory = new SparseArray<>();
+    private long lastPeriod = 0;
+    private static final long ONE_MINUTE = 60 * 1000;
+    private static final int HISTORY_SIZE = 100;
+    private static final int FLAG_LAST_STATE = 2;
+    private static final int FLAG_PARTIAL_ACTIVE = 1;
+    private static final long PERIOD_DURATION = UsageStatsService.DEBUG ? ONE_MINUTE
+            : 60 * ONE_MINUTE;
+
+    public void addEntry(String packageName, int userId, boolean idle, long timeNow) {
+        ArrayMap<String, byte[]> userHistory = idleHistory.get(userId);
+        if (userHistory == null) {
+            userHistory = new ArrayMap<>();
+            idleHistory.put(userId, userHistory);
+        }
+        byte[] packageHistory = userHistory.get(packageName);
+        if (packageHistory == null) {
+            packageHistory = new byte[HISTORY_SIZE];
+            userHistory.put(packageName, packageHistory);
+        }
+        long thisPeriod = timeNow / PERIOD_DURATION;
+        // Has the period switched over? Slide all users' package histories
+        if (lastPeriod != 0 && lastPeriod < thisPeriod
+                && (thisPeriod - lastPeriod) < HISTORY_SIZE - 1) {
+            int diff = (int) (thisPeriod - lastPeriod);
+            final int NUSERS = idleHistory.size();
+            for (int u = 0; u < NUSERS; u++) {
+                userHistory = idleHistory.valueAt(u);
+                for (byte[] history : userHistory.values()) {
+                    // Shift left
+                    System.arraycopy(history, diff, history, 0, HISTORY_SIZE - diff);
+                    // Replicate last state across the diff
+                    for (int i = 0; i < diff; i++) {
+                        history[HISTORY_SIZE - i - 1] =
+                                (byte) (history[HISTORY_SIZE - diff - 1] & FLAG_LAST_STATE);
+                    }
+                }
+            }
+        }
+        lastPeriod = thisPeriod;
+        if (!idle) {
+            packageHistory[HISTORY_SIZE - 1] = FLAG_LAST_STATE | FLAG_PARTIAL_ACTIVE;
+        } else {
+            packageHistory[HISTORY_SIZE - 1] &= ~FLAG_LAST_STATE;
+        }
+    }
+
+    public void removeUser(int userId) {
+        idleHistory.remove(userId);
+    }
+
+    public void dump(IndentingPrintWriter idpw, int userId) {
+        ArrayMap<String, byte[]> userHistory = idleHistory.get(userId);
+        if (userHistory == null) return;
+        final int P = userHistory.size();
+        for (int p = 0; p < P; p++) {
+            final String packageName = userHistory.keyAt(p);
+            final byte[] history = userHistory.valueAt(p);
+            for (int i = 0; i < HISTORY_SIZE; i++) {
+                idpw.print(history[i] == 0 ? '.' : 'A');
+            }
+            idpw.print("  " + packageName);
+            idpw.println();
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index d18f7c2..bfd6cc0 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -64,6 +64,7 @@
 import android.util.SparseArray;
 import android.view.Display;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.DeviceIdleController;
@@ -90,20 +91,21 @@
     static final String TAG = "UsageStatsService";
 
     static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
     private static final long TEN_SECONDS = 10 * 1000;
     private static final long ONE_MINUTE = 60 * 1000;
     private static final long TWENTY_MINUTES = 20 * 60 * 1000;
     private static final long FLUSH_INTERVAL = DEBUG ? TEN_SECONDS : TWENTY_MINUTES;
     private static final long TIME_CHANGE_THRESHOLD_MILLIS = 2 * 1000; // Two seconds.
 
-    static final long DEFAULT_APP_IDLE_THRESHOLD_MILLIS = DEBUG ? ONE_MINUTE * 4 
-            : 1L * 24 * 60 * 60 * 1000; // 1 day
-    static final long DEFAULT_CHECK_IDLE_INTERVAL = DEBUG ? ONE_MINUTE
-            : 8 * 3600 * 1000; // 8 hours
+    static final long DEFAULT_APP_IDLE_THRESHOLD_MILLIS = DEBUG ? ONE_MINUTE * 4
+            : 1L * 24 * 60 * ONE_MINUTE; // 1 day
+    static final long DEFAULT_CHECK_IDLE_INTERVAL = DEBUG ? ONE_MINUTE / 4
+            : 8 * 60 * ONE_MINUTE; // 8 hours
     static final long DEFAULT_PAROLE_INTERVAL = DEBUG ? ONE_MINUTE * 10
-            : 24 * 60 * 60 * 1000L; // 24 hours between paroles
-    static final long DEFAULT_PAROLE_DURATION = DEBUG ? ONE_MINUTE * 2
-            : 10 * 60 * 1000L; // 10 minutes
+            : 24 * 60 * ONE_MINUTE; // 24 hours between paroles
+    static final long DEFAULT_PAROLE_DURATION = DEBUG ? ONE_MINUTE
+            : 10 * ONE_MINUTE; // 10 minutes
 
     // Handler message types.
     static final int MSG_REPORT_EVENT = 0;
@@ -137,6 +139,9 @@
     long mScreenOnTime;
     long mScreenOnSystemTimeSnapshot;
 
+    @GuardedBy("mLock")
+    private AppIdleHistory mAppIdleHistory = new AppIdleHistory();
+
     private ArrayList<UsageStatsManagerInternal.AppIdleStateChangeListener>
             mPackageAccessListeners = new ArrayList<>();
 
@@ -346,12 +351,14 @@
                                 | PackageManager.GET_UNINSTALLED_PACKAGES,
                             userId);
             synchronized (mLock) {
+                final long timeNow = checkAndGetTimeLocked();
                 final int packageCount = packages.size();
                 for (int p = 0; p < packageCount; p++) {
                     final String packageName = packages.get(p).packageName;
                     final boolean isIdle = isAppIdleFiltered(packageName, userId);
                     mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS,
                             userId, isIdle ? 1 : 0, packageName));
+                    mAppIdleHistory.addEntry(packageName, userId, isIdle, timeNow);
                 }
             }
         }
@@ -541,6 +548,7 @@
                     // Slog.d(TAG, "Informing listeners of out-of-idle " + event.mPackage);
                     mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS, userId,
                             /* idle = */ 0, event.mPackage));
+                    mAppIdleHistory.addEntry(event.mPackage, userId, false, timeNow);
                 }
             }
         }
@@ -567,6 +575,7 @@
                 // Slog.d(TAG, "Informing listeners of out-of-idle " + event.mPackage);
                 mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS, userId,
                         /* idle = */ idle ? 1 : 0, packageName));
+                mAppIdleHistory.addEntry(packageName, userId, idle, timeNow);
             }
         }
     }
@@ -768,10 +777,14 @@
                     mUserState.valueAt(i).checkin(idpw, screenOnTime);
                 } else {
                     mUserState.valueAt(i).dump(idpw, screenOnTime);
+                    idpw.println();
+                    if (args.length > 0 && "history".equals(args[0])) {
+                        mAppIdleHistory.dump(idpw, mUserState.keyAt(i));
+                    }
                 }
                 idpw.decreaseIndent();
             }
-            pw.write("Screen On Timestamp:" + mScreenOnTime + "\n");
+            pw.write("Screen On Timebase:" + mScreenOnTime + "\n");
         }
     }