Merge "Add dumpsys output to UsageStatsService, along with --checkin support" into lmp-mr1-dev
diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java
index acdd87e..14af584 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -1371,7 +1371,7 @@
             }
         }
 
-        if (!config.locale.getLanguage().isEmpty()) {
+        if (config.locale != null && !config.locale.getLanguage().isEmpty()) {
             parts.add(localeToResourceQualifier(config.locale));
         }
 
diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
index cfa4436..11da380 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
@@ -39,6 +39,7 @@
 
     private static final String TAG = "UsageStatsDatabase";
     private static final boolean DEBUG = UsageStatsService.DEBUG;
+    private static final String BAK_SUFFIX = ".bak";
 
     private final Object mLock = new Object();
     private final File[] mIntervalDirs;
@@ -95,11 +96,71 @@
         }
     }
 
+    public interface CheckinAction {
+        boolean checkin(IntervalStats stats);
+    }
+
+    /**
+     * Calls {@link CheckinAction#checkin(IntervalStats)} on the given {@link CheckinAction}
+     * for all {@link IntervalStats} that haven't been checked-in.
+     * If any of the calls to {@link CheckinAction#checkin(IntervalStats)} returns false or throws
+     * an exception, the check-in will be aborted.
+     *
+     * @param checkinAction The callback to run when checking-in {@link IntervalStats}.
+     * @return true if the check-in succeeded.
+     */
+    public boolean checkinDailyFiles(CheckinAction checkinAction) {
+        synchronized (mLock) {
+            final TimeSparseArray<AtomicFile> files =
+                    mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY];
+            final int fileCount = files.size();
+            int start = 0;
+            while (start < fileCount - 1) {
+                if (!files.valueAt(start).getBaseFile().getName().endsWith("-c")) {
+                    break;
+                }
+            }
+
+            if (start == fileCount - 1) {
+                return true;
+            }
+
+            try {
+                IntervalStats stats = new IntervalStats();
+                for (int i = start; i < fileCount - 1; i++) {
+                    UsageStatsXml.read(files.valueAt(i), stats);
+                    if (!checkinAction.checkin(stats)) {
+                        return false;
+                    }
+                }
+            } catch (IOException e) {
+                Slog.e(TAG, "Failed to check-in", e);
+                return false;
+            }
+
+            // We have successfully checked-in the stats, so rename the files so that they
+            // are marked as checked-in.
+            for (int i = start; i < fileCount - 1; i++) {
+                final AtomicFile file = files.valueAt(i);
+                final File checkedInFile = new File(file.getBaseFile().getParent(),
+                        file.getBaseFile().getName() + "-c");
+                if (!file.getBaseFile().renameTo(checkedInFile)) {
+                    // We must return success, as we've already marked some files as checked-in.
+                    // It's better to repeat ourselves than to lose data.
+                    Slog.e(TAG, "Failed to mark file " + file.getBaseFile().getPath()
+                            + " as checked-in");
+                    return true;
+                }
+            }
+        }
+        return true;
+    }
+
     private void indexFilesLocked() {
         final FilenameFilter backupFileFilter = new FilenameFilter() {
             @Override
             public boolean accept(File dir, String name) {
-                return !name.endsWith(".bak");
+                return !name.endsWith(BAK_SUFFIX);
             }
         };
 
@@ -383,10 +444,10 @@
         if (files != null) {
             for (File f : files) {
                 String path = f.getPath();
-                if (path.endsWith(".bak")) {
-                    f = new File(path.substring(0, path.length() - 4));
+                if (path.endsWith(BAK_SUFFIX)) {
+                    f = new File(path.substring(0, path.length() - BAK_SUFFIX.length()));
                 }
-                long beginTime = Long.parseLong(f.getName());
+                long beginTime = UsageStatsXml.parseBeginTime(f);
                 if (beginTime < expiryTime) {
                     new AtomicFile(f).delete();
                 }
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 7ff246a..485b2a2 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -33,7 +33,6 @@
 import android.content.pm.UserInfo;
 import android.content.res.Configuration;
 import android.os.Binder;
-import android.os.Debug;
 import android.os.Environment;
 import android.os.Handler;
 import android.os.Looper;
@@ -48,9 +47,12 @@
 import android.util.SparseArray;
 
 import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.SystemService;
 
 import java.io.File;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.List;
 
@@ -177,7 +179,7 @@
             long currentTimeMillis) {
         UserUsageStatsService service = mUserState.get(userId);
         if (service == null) {
-            service = new UserUsageStatsService(userId,
+            service = new UserUsageStatsService(getContext(), userId,
                     new File(mUsageStatsDir, Integer.toString(userId)), this);
             service.init(currentTimeMillis);
             mUserState.put(userId, service);
@@ -320,6 +322,30 @@
         mHandler.removeMessages(MSG_FLUSH_TO_DISK);
     }
 
+    /**
+     * Called by the Binder stub.
+     */
+    void dump(String[] args, PrintWriter pw) {
+        synchronized (mLock) {
+            IndentingPrintWriter idpw = new IndentingPrintWriter(pw, "  ");
+            ArraySet<String> argSet = new ArraySet<>();
+            argSet.addAll(Arrays.asList(args));
+
+            final int userCount = mUserState.size();
+            for (int i = 0; i < userCount; i++) {
+                idpw.printPair("user", mUserState.keyAt(i));
+                idpw.println();
+                idpw.increaseIndent();
+                if (argSet.contains("--checkin")) {
+                    mUserState.valueAt(i).checkin(idpw);
+                } else {
+                    mUserState.valueAt(i).dump(idpw);
+                }
+                idpw.decreaseIndent();
+            }
+        }
+    }
+
     class H extends Handler {
         public H(Looper looper) {
             super(looper);
@@ -422,6 +448,18 @@
                 Binder.restoreCallingIdentity(token);
             }
         }
+
+        @Override
+        protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                    != PackageManager.PERMISSION_GRANTED) {
+                pw.println("Permission Denial: can't dump UsageStats from pid="
+                        + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+                        + " without permission " + android.Manifest.permission.DUMP);
+                return;
+            }
+            UsageStatsService.this.dump(args, pw);
+        }
     }
 
     /**
diff --git a/services/usage/java/com/android/server/usage/UsageStatsXml.java b/services/usage/java/com/android/server/usage/UsageStatsXml.java
index 9ce6d63..26148ce 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsXml.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsXml.java
@@ -24,21 +24,26 @@
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import java.io.*;
 
 public class UsageStatsXml {
     private static final String TAG = "UsageStatsXml";
     private static final int CURRENT_VERSION = 1;
     private static final String USAGESTATS_TAG = "usagestats";
     private static final String VERSION_ATTR = "version";
+    private static final String CHECKED_IN_SUFFIX = "-c";
 
     public static long parseBeginTime(AtomicFile file) {
-        return Long.parseLong(file.getBaseFile().getName());
+        return parseBeginTime(file.getBaseFile());
+    }
+
+    public static long parseBeginTime(File file) {
+        final String name = file.getName();
+        if (name.endsWith(CHECKED_IN_SUFFIX)) {
+            return Long.parseLong(
+                    name.substring(0, name.length() - CHECKED_IN_SUFFIX.length()));
+        }
+        return Long.parseLong(name);
     }
 
     public static void read(AtomicFile file, IntervalStats statsOut) throws IOException {
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index 4916ec2..6596781 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -23,9 +23,13 @@
 import android.app.usage.UsageStatsManager;
 import android.content.res.Configuration;
 import android.os.SystemClock;
+import android.content.Context;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Slog;
 
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.usage.UsageStatsDatabase.StatCombiner;
 
 import java.io.File;
@@ -43,7 +47,13 @@
     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 static final int sDateFormatFlags =
+            DateUtils.FORMAT_SHOW_DATE
+            | DateUtils.FORMAT_SHOW_TIME
+            | DateUtils.FORMAT_SHOW_YEAR
+            | DateUtils.FORMAT_NUMERIC_DATE;
 
+    private final Context mContext;
     private final UsageStatsDatabase mDatabase;
     private final IntervalStats[] mCurrentStats;
     private boolean mStatsChanged = false;
@@ -55,7 +65,8 @@
         void onStatsUpdated();
     }
 
-    UserUsageStatsService(int userId, File usageStatsDir, StatsUpdatedListener listener) {
+    UserUsageStatsService(Context context, int userId, File usageStatsDir, StatsUpdatedListener listener) {
+        mContext = context;
         mDailyExpiryDate = new UnixCalendar(0);
         mDatabase = new UsageStatsDatabase(usageStatsDir);
         mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
@@ -433,6 +444,117 @@
                 tempCal.getTimeInMillis() + ")");
     }
 
+    //
+    // -- DUMP related methods --
+    //
+
+    void checkin(final IndentingPrintWriter pw) {
+        mDatabase.checkinDailyFiles(new UsageStatsDatabase.CheckinAction() {
+            @Override
+            public boolean checkin(IntervalStats stats) {
+                printIntervalStats(pw, stats, false);
+                return true;
+            }
+        });
+    }
+
+    void dump(IndentingPrintWriter pw) {
+        // This is not a check-in, only dump in-memory stats.
+        for (int interval = 0; interval < mCurrentStats.length; interval++) {
+            pw.print("In-memory ");
+            pw.print(intervalToString(interval));
+            pw.println(" stats");
+            printIntervalStats(pw, mCurrentStats[interval], true);
+        }
+    }
+
+    private String formatDateTime(long dateTime, boolean pretty) {
+        if (pretty) {
+            return "\"" + DateUtils.formatDateTime(mContext, dateTime, sDateFormatFlags) + "\"";
+        }
+        return Long.toString(dateTime);
+    }
+
+    private String formatElapsedTime(long elapsedTime, boolean pretty) {
+        if (pretty) {
+            return "\"" + DateUtils.formatElapsedTime(elapsedTime / 1000) + "\"";
+        }
+        return Long.toString(elapsedTime);
+    }
+
+    void printIntervalStats(IndentingPrintWriter pw, IntervalStats stats, boolean prettyDates) {
+        if (prettyDates) {
+            pw.printPair("timeRange", "\"" + DateUtils.formatDateRange(mContext,
+                    stats.beginTime, stats.endTime, sDateFormatFlags) + "\"");
+        } else {
+            pw.printPair("beginTime", stats.beginTime);
+            pw.printPair("endTime", stats.endTime);
+        }
+        pw.println();
+        pw.increaseIndent();
+        pw.println("packages");
+        pw.increaseIndent();
+        final ArrayMap<String, UsageStats> pkgStats = stats.packageStats;
+        final int pkgCount = pkgStats.size();
+        for (int i = 0; i < pkgCount; i++) {
+            final UsageStats usageStats = pkgStats.valueAt(i);
+            pw.printPair("package", usageStats.mPackageName);
+            pw.printPair("totalTime", formatElapsedTime(usageStats.mTotalTimeInForeground, prettyDates));
+            pw.printPair("lastTime", formatDateTime(usageStats.mLastTimeUsed, prettyDates));
+            pw.println();
+        }
+        pw.decreaseIndent();
+
+        pw.println("configurations");
+        pw.increaseIndent();
+        final ArrayMap<Configuration, ConfigurationStats> configStats =
+                stats.configurations;
+        final int configCount = configStats.size();
+        for (int i = 0; i < configCount; i++) {
+            final ConfigurationStats config = configStats.valueAt(i);
+            pw.printPair("config", Configuration.resourceQualifierString(config.mConfiguration));
+            pw.printPair("totalTime", formatElapsedTime(config.mTotalTimeActive, prettyDates));
+            pw.printPair("lastTime", formatDateTime(config.mLastTimeActive, prettyDates));
+            pw.printPair("count", config.mActivationCount);
+            pw.println();
+        }
+        pw.decreaseIndent();
+
+        pw.println("events");
+        pw.increaseIndent();
+        final TimeSparseArray<UsageEvents.Event> events = stats.events;
+        final int eventCount = events != null ? events.size() : 0;
+        for (int i = 0; i < eventCount; i++) {
+            final UsageEvents.Event event = events.valueAt(i);
+            pw.printPair("time", formatDateTime(event.mTimeStamp, prettyDates));
+            pw.printPair("type", eventToString(event.mEventType));
+            pw.printPair("package", event.mPackage);
+            if (event.mClass != null) {
+                pw.printPair("class", event.mClass);
+            }
+            if (event.mConfiguration != null) {
+                pw.printPair("config", Configuration.resourceQualifierString(event.mConfiguration));
+            }
+            pw.println();
+        }
+        pw.decreaseIndent();
+        pw.decreaseIndent();
+    }
+
+    private static String intervalToString(int interval) {
+        switch (interval) {
+            case UsageStatsManager.INTERVAL_DAILY:
+                return "daily";
+            case UsageStatsManager.INTERVAL_WEEKLY:
+                return "weekly";
+            case UsageStatsManager.INTERVAL_MONTHLY:
+                return "monthly";
+            case UsageStatsManager.INTERVAL_YEARLY:
+                return "yearly";
+            default:
+                return "?";
+        }
+    }
 
     private static String eventToString(int eventType) {
         switch (eventType) {