Lightweight checkin output for network stats.

Define and print a compact version of network statistics when dump
is requested with the "--checkin" flag.  Defaults to last 24 hours,
but included data can be tweaked with various flags.

Groups together detailed network identities into larger umbrella
terms like "mobile" and "wifi."

Bug: 18415963
Change-Id: I70cf9c828ea5c6e5bb6884837d3608f66fbad2e6
diff --git a/core/java/android/net/NetworkIdentity.java b/core/java/android/net/NetworkIdentity.java
index 36dd2fdfb..d36707e 100644
--- a/core/java/android/net/NetworkIdentity.java
+++ b/core/java/android/net/NetworkIdentity.java
@@ -34,7 +34,7 @@
  *
  * @hide
  */
-public class NetworkIdentity {
+public class NetworkIdentity implements Comparable<NetworkIdentity> {
     /**
      * When enabled, combine all {@link #mSubType} together under
      * {@link #SUBTYPE_COMBINED}.
@@ -76,7 +76,7 @@
 
     @Override
     public String toString() {
-        final StringBuilder builder = new StringBuilder("[");
+        final StringBuilder builder = new StringBuilder("{");
         builder.append("type=").append(getNetworkTypeName(mType));
         builder.append(", subType=");
         if (COMBINE_SUBTYPE_ENABLED) {
@@ -95,7 +95,7 @@
         if (mRoaming) {
             builder.append(", ROAMING");
         }
-        return builder.append("]").toString();
+        return builder.append("}").toString();
     }
 
     public int getType() {
@@ -170,4 +170,22 @@
 
         return new NetworkIdentity(type, subType, subscriberId, networkId, roaming);
     }
+
+    @Override
+    public int compareTo(NetworkIdentity another) {
+        int res = Integer.compare(mType, another.mType);
+        if (res == 0) {
+            res = Integer.compare(mSubType, another.mSubType);
+        }
+        if (res == 0 && mSubscriberId != null && another.mSubscriberId != null) {
+            res = mSubscriberId.compareTo(another.mSubscriberId);
+        }
+        if (res == 0 && mNetworkId != null && another.mNetworkId != null) {
+            res = mNetworkId.compareTo(another.mNetworkId);
+        }
+        if (res == 0) {
+            res = Boolean.compare(mRoaming, another.mRoaming);
+        }
+        return res;
+    }
 }
diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java
index ea5dfd1..2afe578 100644
--- a/core/java/android/net/NetworkStats.java
+++ b/core/java/android/net/NetworkStats.java
@@ -733,6 +733,22 @@
     }
 
     /**
+     * Return text description of {@link #set} value.
+     */
+    public static String setToCheckinString(int set) {
+        switch (set) {
+            case SET_ALL:
+                return "all";
+            case SET_DEFAULT:
+                return "def";
+            case SET_FOREGROUND:
+                return "fg";
+            default:
+                return "unk";
+        }
+    }
+
+    /**
      * Return text description of {@link #tag} value.
      */
     public static String tagToString(int tag) {
diff --git a/core/java/android/net/NetworkStatsHistory.java b/core/java/android/net/NetworkStatsHistory.java
index 62d8738..4a4accb 100644
--- a/core/java/android/net/NetworkStatsHistory.java
+++ b/core/java/android/net/NetworkStatsHistory.java
@@ -26,6 +26,7 @@
 import static android.net.NetworkStatsHistory.Entry.UNKNOWN;
 import static android.net.NetworkStatsHistory.ParcelUtils.readLongArray;
 import static android.net.NetworkStatsHistory.ParcelUtils.writeLongArray;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
 import static com.android.internal.util.ArrayUtils.total;
 
 import android.os.Parcel;
@@ -38,6 +39,7 @@
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.net.ProtocolException;
 import java.util.Arrays;
 import java.util.Random;
@@ -573,8 +575,22 @@
         return (long) (start + (r.nextFloat() * (end - start)));
     }
 
+    /**
+     * Quickly determine if this history intersects with given window.
+     */
+    public boolean intersects(long start, long end) {
+        final long dataStart = getStart();
+        final long dataEnd = getEnd();
+        if (start >= dataStart && start <= dataEnd) return true;
+        if (end >= dataStart && end <= dataEnd) return true;
+        if (dataStart >= start && dataStart <= end) return true;
+        if (dataEnd >= start && dataEnd <= end) return true;
+        return false;
+    }
+
     public void dump(IndentingPrintWriter pw, boolean fullHistory) {
-        pw.print("NetworkStatsHistory: bucketDuration="); pw.println(bucketDuration);
+        pw.print("NetworkStatsHistory: bucketDuration=");
+        pw.println(bucketDuration / SECOND_IN_MILLIS);
         pw.increaseIndent();
 
         final int start = fullHistory ? 0 : Math.max(0, bucketCount - 32);
@@ -583,19 +599,35 @@
         }
 
         for (int i = start; i < bucketCount; i++) {
-            pw.print("bucketStart="); pw.print(bucketStart[i]);
-            if (activeTime != null) { pw.print(" activeTime="); pw.print(activeTime[i]); }
-            if (rxBytes != null) { pw.print(" rxBytes="); pw.print(rxBytes[i]); }
-            if (rxPackets != null) { pw.print(" rxPackets="); pw.print(rxPackets[i]); }
-            if (txBytes != null) { pw.print(" txBytes="); pw.print(txBytes[i]); }
-            if (txPackets != null) { pw.print(" txPackets="); pw.print(txPackets[i]); }
-            if (operations != null) { pw.print(" operations="); pw.print(operations[i]); }
+            pw.print("st="); pw.print(bucketStart[i] / SECOND_IN_MILLIS);
+            if (rxBytes != null) { pw.print(" rb="); pw.print(rxBytes[i]); }
+            if (rxPackets != null) { pw.print(" rp="); pw.print(rxPackets[i]); }
+            if (txBytes != null) { pw.print(" tb="); pw.print(txBytes[i]); }
+            if (txPackets != null) { pw.print(" tp="); pw.print(txPackets[i]); }
+            if (operations != null) { pw.print(" op="); pw.print(operations[i]); }
             pw.println();
         }
 
         pw.decreaseIndent();
     }
 
+    public void dumpCheckin(PrintWriter pw) {
+        pw.print("d,");
+        pw.print(bucketDuration / SECOND_IN_MILLIS);
+        pw.println();
+
+        for (int i = 0; i < bucketCount; i++) {
+            pw.print("b,");
+            pw.print(bucketStart[i] / SECOND_IN_MILLIS); pw.print(',');
+            if (rxBytes != null) { pw.print(rxBytes[i]); } else { pw.print("*"); } pw.print(',');
+            if (rxPackets != null) { pw.print(rxPackets[i]); } else { pw.print("*"); } pw.print(',');
+            if (txBytes != null) { pw.print(txBytes[i]); } else { pw.print("*"); } pw.print(',');
+            if (txPackets != null) { pw.print(txPackets[i]); } else { pw.print("*"); } pw.print(',');
+            if (operations != null) { pw.print(operations[i]); } else { pw.print("*"); }
+            pw.println();
+        }
+    }
+
     @Override
     public String toString() {
         final CharArrayWriter writer = new CharArrayWriter();
diff --git a/core/java/android/net/NetworkTemplate.java b/core/java/android/net/NetworkTemplate.java
index 27197cc..b839e0a 100644
--- a/core/java/android/net/NetworkTemplate.java
+++ b/core/java/android/net/NetworkTemplate.java
@@ -16,6 +16,7 @@
 
 package android.net;
 
+import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
 import static android.net.ConnectivityManager.TYPE_ETHERNET;
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
@@ -34,10 +35,10 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
-import java.util.Objects;
-
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.Objects;
+
 /**
  * Template definition used to generically match {@link NetworkIdentity},
  * usually when collecting statistics.
@@ -53,6 +54,7 @@
     public static final int MATCH_ETHERNET = 5;
     public static final int MATCH_MOBILE_WILDCARD = 6;
     public static final int MATCH_WIFI_WILDCARD = 7;
+    public static final int MATCH_BLUETOOTH = 8;
 
     /**
      * Set of {@link NetworkInfo#getType()} that reflect data usage.
@@ -134,6 +136,14 @@
         return new NetworkTemplate(MATCH_ETHERNET, null, null);
     }
 
+    /**
+     * Template to combine all {@link ConnectivityManager#TYPE_BLUETOOTH} style
+     * networks together.
+     */
+    public static NetworkTemplate buildTemplateBluetooth() {
+        return new NetworkTemplate(MATCH_BLUETOOTH, null, null);
+    }
+
     private final int mMatchRule;
     private final String mSubscriberId;
     private final String mNetworkId;
@@ -222,6 +232,8 @@
                 return matchesMobileWildcard(ident);
             case MATCH_WIFI_WILDCARD:
                 return matchesWifiWildcard(ident);
+            case MATCH_BLUETOOTH:
+                return matchesBluetooth(ident);
             default:
                 throw new IllegalArgumentException("unknown network template");
         }
@@ -316,6 +328,16 @@
         }
     }
 
+    /**
+     * Check if matches Bluetooth network template.
+     */
+    private boolean matchesBluetooth(NetworkIdentity ident) {
+        if (ident.mType == TYPE_BLUETOOTH) {
+            return true;
+        }
+        return false;
+    }
+
     private static String getMatchRuleName(int matchRule) {
         switch (matchRule) {
             case MATCH_MOBILE_3G_LOWER:
@@ -332,6 +354,8 @@
                 return "MOBILE_WILDCARD";
             case MATCH_WIFI_WILDCARD:
                 return "WIFI_WILDCARD";
+            case MATCH_BLUETOOTH:
+                return "BLUETOOTH";
             default:
                 return "UNKNOWN";
         }
diff --git a/core/tests/coretests/src/android/net/NetworkStatsHistoryTest.java b/core/tests/coretests/src/android/net/NetworkStatsHistoryTest.java
index b181122..9a08f41 100644
--- a/core/tests/coretests/src/android/net/NetworkStatsHistoryTest.java
+++ b/core/tests/coretests/src/android/net/NetworkStatsHistoryTest.java
@@ -451,6 +451,40 @@
         assertIndexBeforeAfter(stats, 4, 4, Long.MAX_VALUE);
     }
 
+    public void testIntersects() throws Exception {
+        final long BUCKET_SIZE = HOUR_IN_MILLIS;
+        stats = new NetworkStatsHistory(BUCKET_SIZE);
+
+        final long FIRST_START = TEST_START;
+        final long FIRST_END = FIRST_START + (2 * HOUR_IN_MILLIS);
+        final long SECOND_START = TEST_START + WEEK_IN_MILLIS;
+        final long SECOND_END = SECOND_START + HOUR_IN_MILLIS;
+        final long THIRD_START = TEST_START + (2 * WEEK_IN_MILLIS);
+        final long THIRD_END = THIRD_START + (2 * HOUR_IN_MILLIS);
+
+        stats.recordData(FIRST_START, FIRST_END,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+        stats.recordData(SECOND_START, SECOND_END,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+        stats.recordData(THIRD_START, THIRD_END,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+
+        assertFalse(stats.intersects(10, 20));
+        assertFalse(stats.intersects(TEST_START + YEAR_IN_MILLIS, TEST_START + YEAR_IN_MILLIS + 1));
+        assertFalse(stats.intersects(Long.MAX_VALUE, Long.MIN_VALUE));
+
+        assertTrue(stats.intersects(Long.MIN_VALUE, Long.MAX_VALUE));
+        assertTrue(stats.intersects(10, TEST_START + YEAR_IN_MILLIS));
+        assertTrue(stats.intersects(TEST_START, TEST_START));
+        assertTrue(stats.intersects(TEST_START + DAY_IN_MILLIS, TEST_START + DAY_IN_MILLIS + 1));
+        assertTrue(stats.intersects(TEST_START + DAY_IN_MILLIS, Long.MAX_VALUE));
+        assertTrue(stats.intersects(TEST_START + 1, Long.MAX_VALUE));
+
+        assertFalse(stats.intersects(Long.MIN_VALUE, TEST_START - 1));
+        assertTrue(stats.intersects(Long.MIN_VALUE, TEST_START));
+        assertTrue(stats.intersects(Long.MIN_VALUE, TEST_START + 1));
+    }
+
     private static void assertIndexBeforeAfter(
             NetworkStatsHistory stats, int before, int after, long time) {
         assertEquals("unexpected before", before, stats.getIndexBefore(time));
diff --git a/services/core/java/com/android/server/net/NetworkIdentitySet.java b/services/core/java/com/android/server/net/NetworkIdentitySet.java
index 397f9f4..f230bb3 100644
--- a/services/core/java/com/android/server/net/NetworkIdentitySet.java
+++ b/services/core/java/com/android/server/net/NetworkIdentitySet.java
@@ -29,7 +29,8 @@
  *
  * @hide
  */
-public class NetworkIdentitySet extends HashSet<NetworkIdentity> {
+public class NetworkIdentitySet extends HashSet<NetworkIdentity> implements
+        Comparable<NetworkIdentitySet> {
     private static final int VERSION_INIT = 1;
     private static final int VERSION_ADD_ROAMING = 2;
     private static final int VERSION_ADD_NETWORK_ID = 3;
@@ -92,4 +93,14 @@
             return null;
         }
     }
+
+    @Override
+    public int compareTo(NetworkIdentitySet another) {
+        if (isEmpty()) return -1;
+        if (another.isEmpty()) return 1;
+
+        final NetworkIdentity ident = iterator().next();
+        final NetworkIdentity anotherIdent = another.iterator().next();
+        return ident.compareTo(anotherIdent);
+    }
 }
diff --git a/services/core/java/com/android/server/net/NetworkStatsCollection.java b/services/core/java/com/android/server/net/NetworkStatsCollection.java
index 475482f..3ac20f7 100644
--- a/services/core/java/com/android/server/net/NetworkStatsCollection.java
+++ b/services/core/java/com/android/server/net/NetworkStatsCollection.java
@@ -22,15 +22,20 @@
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.TrafficStats.UID_REMOVED;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+import static android.text.format.DateUtils.WEEK_IN_MILLIS;
 
+import android.net.ConnectivityManager;
 import android.net.NetworkIdentity;
 import android.net.NetworkStats;
 import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
 import android.net.TrafficStats;
-import android.text.format.DateUtils;
+import android.util.ArrayMap;
 import android.util.AtomicFile;
 
+import libcore.io.IoUtils;
+
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FileRotator;
 import com.android.internal.util.IndentingPrintWriter;
@@ -44,15 +49,13 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.PrintWriter;
 import java.net.ProtocolException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.Map;
 import java.util.Objects;
 
-import libcore.io.IoUtils;
-
 /**
  * Collection of {@link NetworkStatsHistory}, stored based on combined key of
  * {@link NetworkIdentitySet}, UID, set, and tag. Knows how to persist itself.
@@ -70,7 +73,7 @@
 
     private static final int VERSION_UNIFIED_INIT = 16;
 
-    private HashMap<Key, NetworkStatsHistory> mStats = Maps.newHashMap();
+    private ArrayMap<Key, NetworkStatsHistory> mStats = new ArrayMap<>();
 
     private final long mBucketDuration;
 
@@ -145,12 +148,13 @@
             NetworkTemplate template, int uid, int set, int tag, int fields, long start, long end) {
         final NetworkStatsHistory combined = new NetworkStatsHistory(
                 mBucketDuration, estimateBuckets(), fields);
-        for (Map.Entry<Key, NetworkStatsHistory> entry : mStats.entrySet()) {
-            final Key key = entry.getKey();
+        for (int i = 0; i < mStats.size(); i++) {
+            final Key key = mStats.keyAt(i);
             final boolean setMatches = set == SET_ALL || key.set == set;
             if (key.uid == uid && setMatches && key.tag == tag
                     && templateMatches(template, key.ident)) {
-                combined.recordHistory(entry.getValue(), start, end);
+                final NetworkStatsHistory value = mStats.valueAt(i);
+                combined.recordHistory(value, start, end);
             }
         }
         return combined;
@@ -170,11 +174,11 @@
         // shortcut when we know stats will be empty
         if (start == end) return stats;
 
-        for (Map.Entry<Key, NetworkStatsHistory> mapEntry : mStats.entrySet()) {
-            final Key key = mapEntry.getKey();
+        for (int i = 0; i < mStats.size(); i++) {
+            final Key key = mStats.keyAt(i);
             if (templateMatches(template, key.ident)) {
-                final NetworkStatsHistory history = mapEntry.getValue();
-                historyEntry = history.getValues(start, end, now, historyEntry);
+                final NetworkStatsHistory value = mStats.valueAt(i);
+                historyEntry = value.getValues(start, end, now, historyEntry);
 
                 entry.iface = IFACE_ALL;
                 entry.uid = key.uid;
@@ -225,8 +229,10 @@
      * into this collection.
      */
     public void recordCollection(NetworkStatsCollection another) {
-        for (Map.Entry<Key, NetworkStatsHistory> entry : another.mStats.entrySet()) {
-            recordHistory(entry.getKey(), entry.getValue());
+        for (int i = 0; i < another.mStats.size(); i++) {
+            final Key key = another.mStats.keyAt(i);
+            final NetworkStatsHistory value = another.mStats.valueAt(i);
+            recordHistory(key, value);
         }
     }
 
@@ -460,7 +466,7 @@
     }
 
     private int estimateBuckets() {
-        return (int) (Math.min(mEndMillis - mStartMillis, DateUtils.WEEK_IN_MILLIS * 5)
+        return (int) (Math.min(mEndMillis - mStartMillis, WEEK_IN_MILLIS * 5)
                 / mBucketDuration);
     }
 
@@ -482,6 +488,54 @@
         }
     }
 
+    public void dumpCheckin(PrintWriter pw, long start, long end) {
+        dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateMobileWildcard(), "cell");
+        dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateWifiWildcard(), "wifi");
+        dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateEthernet(), "eth");
+        dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateBluetooth(), "bt");
+    }
+
+    /**
+     * Dump all contained stats that match requested parameters, but group
+     * together all matching {@link NetworkTemplate} under a single prefix.
+     */
+    private void dumpCheckin(PrintWriter pw, long start, long end, NetworkTemplate groupTemplate,
+            String groupPrefix) {
+        final ArrayMap<Key, NetworkStatsHistory> grouped = new ArrayMap<>();
+
+        // Walk through all history, grouping by matching network templates
+        for (int i = 0; i < mStats.size(); i++) {
+            final Key key = mStats.keyAt(i);
+            final NetworkStatsHistory value = mStats.valueAt(i);
+
+            if (!templateMatches(groupTemplate, key.ident)) continue;
+
+            final Key groupKey = new Key(null, key.uid, key.set, key.tag);
+            NetworkStatsHistory groupHistory = grouped.get(groupKey);
+            if (groupHistory == null) {
+                groupHistory = new NetworkStatsHistory(value.getBucketDuration());
+                grouped.put(groupKey, groupHistory);
+            }
+            groupHistory.recordHistory(value, start, end);
+        }
+
+        for (int i = 0; i < grouped.size(); i++) {
+            final Key key = grouped.keyAt(i);
+            final NetworkStatsHistory value = grouped.valueAt(i);
+
+            if (value.size() == 0) continue;
+
+            pw.print("c,");
+            pw.print(groupPrefix); pw.print(',');
+            pw.print(key.uid); pw.print(',');
+            pw.print(NetworkStats.setToCheckinString(key.set)); pw.print(',');
+            pw.print(key.tag);
+            pw.println();
+
+            value.dumpCheckin(pw);
+        }
+    }
+
     /**
      * Test if given {@link NetworkTemplate} matches any {@link NetworkIdentity}
      * in the given {@link NetworkIdentitySet}.
@@ -528,7 +582,20 @@
 
         @Override
         public int compareTo(Key another) {
-            return Integer.compare(uid, another.uid);
+            int res = 0;
+            if (ident != null && another.ident != null) {
+                res = ident.compareTo(another.ident);
+            }
+            if (res == 0) {
+                res = Integer.compare(uid, another.uid);
+            }
+            if (res == 0) {
+                res = Integer.compare(set, another.set);
+            }
+            if (res == 0) {
+                res = Integer.compare(tag, another.tag);
+            }
+            return res;
         }
     }
 }
diff --git a/services/core/java/com/android/server/net/NetworkStatsRecorder.java b/services/core/java/com/android/server/net/NetworkStatsRecorder.java
index cea084b..d5d7667 100644
--- a/services/core/java/com/android/server/net/NetworkStatsRecorder.java
+++ b/services/core/java/com/android/server/net/NetworkStatsRecorder.java
@@ -41,6 +41,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.Arrays;
 import java.util.HashSet;
@@ -124,23 +125,36 @@
      * as reference is valid.
      */
     public NetworkStatsCollection getOrLoadCompleteLocked() {
-        NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
-        if (complete == null) {
-            if (LOGD) Slog.d(TAG, "getOrLoadCompleteLocked() reading from disk for " + mCookie);
-            try {
-                complete = new NetworkStatsCollection(mBucketDuration);
-                mRotator.readMatching(complete, Long.MIN_VALUE, Long.MAX_VALUE);
-                complete.recordCollection(mPending);
-                mComplete = new WeakReference<NetworkStatsCollection>(complete);
-            } catch (IOException e) {
-                Log.wtf(TAG, "problem completely reading network stats", e);
-                recoverFromWtf();
-            } catch (OutOfMemoryError e) {
-                Log.wtf(TAG, "problem completely reading network stats", e);
-                recoverFromWtf();
-            }
+        NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
+        if (res == null) {
+            res = loadLocked(Long.MIN_VALUE, Long.MAX_VALUE);
+            mComplete = new WeakReference<NetworkStatsCollection>(res);
         }
-        return complete;
+        return res;
+    }
+
+    public NetworkStatsCollection getOrLoadPartialLocked(long start, long end) {
+        NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
+        if (res == null) {
+            res = loadLocked(start, end);
+        }
+        return res;
+    }
+
+    private NetworkStatsCollection loadLocked(long start, long end) {
+        if (LOGD) Slog.d(TAG, "loadLocked() reading from disk for " + mCookie);
+        final NetworkStatsCollection res = new NetworkStatsCollection(mBucketDuration);
+        try {
+            mRotator.readMatching(res, start, end);
+            res.recordCollection(mPending);
+        } catch (IOException e) {
+            Log.wtf(TAG, "problem completely reading network stats", e);
+            recoverFromWtf();
+        } catch (OutOfMemoryError e) {
+            Log.wtf(TAG, "problem completely reading network stats", e);
+            recoverFromWtf();
+        }
+        return res;
     }
 
     /**
@@ -384,6 +398,11 @@
         }
     }
 
+    public void dumpCheckin(PrintWriter pw, long start, long end) {
+        // Only load and dump stats from the requested window
+        getOrLoadPartialLocked(start, end).dumpCheckin(pw, start, end);
+    }
+
     /**
      * Recover from {@link FileRotator} failure by dumping state to
      * {@link DropBoxManager} and deleting contents.
diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/services/core/java/com/android/server/net/NetworkStatsService.java
index 150ad34..61f9a26 100644
--- a/services/core/java/com/android/server/net/NetworkStatsService.java
+++ b/services/core/java/com/android/server/net/NetworkStatsService.java
@@ -104,6 +104,7 @@
 import android.provider.Settings.Global;
 import android.telephony.PhoneStateListener;
 import android.telephony.TelephonyManager;
+import android.text.format.DateUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.EventLog;
@@ -1094,12 +1095,20 @@
     }
 
     @Override
-    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+    protected void dump(FileDescriptor fd, PrintWriter rawWriter, String[] args) {
         mContext.enforceCallingOrSelfPermission(DUMP, TAG);
 
+        long duration = DateUtils.DAY_IN_MILLIS;
         final HashSet<String> argSet = new HashSet<String>();
         for (String arg : args) {
             argSet.add(arg);
+
+            if (arg.startsWith("--duration=")) {
+                try {
+                    duration = Long.parseLong(arg.substring(11));
+                } catch (NumberFormatException ignored) {
+                }
+            }
         }
 
         // usage: dumpsys netstats --full --uid --tag --poll --checkin
@@ -1109,7 +1118,7 @@
         final boolean includeUid = argSet.contains("--uid") || argSet.contains("detail");
         final boolean includeTag = argSet.contains("--tag") || argSet.contains("detail");
 
-        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
+        final IndentingPrintWriter pw = new IndentingPrintWriter(rawWriter, "  ");
 
         synchronized (mStatsLock) {
             if (poll) {
@@ -1119,13 +1128,24 @@
             }
 
             if (checkin) {
-                // list current stats files to verify rotation
-                pw.println("Current files:");
-                pw.increaseIndent();
-                for (String file : mBaseDir.list()) {
-                    pw.println(file);
+                final long end = System.currentTimeMillis();
+                final long start = end - duration;
+
+                pw.print("v1,");
+                pw.print(start / SECOND_IN_MILLIS); pw.print(',');
+                pw.print(end / SECOND_IN_MILLIS); pw.println();
+
+                pw.println("xt");
+                mXtRecorder.dumpCheckin(rawWriter, start, end);
+
+                if (includeUid) {
+                    pw.println("uid");
+                    mUidRecorder.dumpCheckin(rawWriter, start, end);
                 }
-                pw.decreaseIndent();
+                if (includeTag) {
+                    pw.println("tag");
+                    mUidTagRecorder.dumpCheckin(rawWriter, start, end);
+                }
                 return;
             }