Merge "NumberPicker should not throw an exception if no enough values for wrapping."
diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java
index e8f60b4..7a1ef66 100644
--- a/core/java/android/net/NetworkStats.java
+++ b/core/java/android/net/NetworkStats.java
@@ -102,6 +102,15 @@
             this.operations = operations;
         }
 
+        public boolean isNegative() {
+            return rxBytes < 0 || rxPackets < 0 || txBytes < 0 || txPackets < 0 || operations < 0;
+        }
+
+        public boolean isEmpty() {
+            return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0
+                    && operations == 0;
+        }
+
         @Override
         public String toString() {
             final StringBuilder builder = new StringBuilder();
@@ -343,6 +352,7 @@
      * on matching {@link #uid} and {@link #tag} rows. Ignores {@link #iface},
      * since operation counts are at data layer.
      */
+    @Deprecated
     public void spliceOperationsFrom(NetworkStats stats) {
         for (int i = 0; i < size; i++) {
             final int j = stats.findIndex(IFACE_ALL, uid[i], set[i], tag[i]);
@@ -397,7 +407,7 @@
      * Return total of all fields represented by this snapshot object.
      */
     public Entry getTotal(Entry recycle) {
-        return getTotal(recycle, null, UID_ALL);
+        return getTotal(recycle, null, UID_ALL, false);
     }
 
     /**
@@ -405,7 +415,7 @@
      * the requested {@link #uid}.
      */
     public Entry getTotal(Entry recycle, int limitUid) {
-        return getTotal(recycle, null, limitUid);
+        return getTotal(recycle, null, limitUid, false);
     }
 
     /**
@@ -413,7 +423,11 @@
      * the requested {@link #iface}.
      */
     public Entry getTotal(Entry recycle, HashSet<String> limitIface) {
-        return getTotal(recycle, limitIface, UID_ALL);
+        return getTotal(recycle, limitIface, UID_ALL, false);
+    }
+
+    public Entry getTotalIncludingTags(Entry recycle) {
+        return getTotal(recycle, null, UID_ALL, true);
     }
 
     /**
@@ -423,7 +437,8 @@
      * @param limitIface Set of {@link #iface} to include in total; or {@code
      *            null} to include all ifaces.
      */
-    private Entry getTotal(Entry recycle, HashSet<String> limitIface, int limitUid) {
+    private Entry getTotal(
+            Entry recycle, HashSet<String> limitIface, int limitUid, boolean includeTags) {
         final Entry entry = recycle != null ? recycle : new Entry();
 
         entry.iface = IFACE_ALL;
@@ -442,7 +457,7 @@
 
             if (matchesUid && matchesIface) {
                 // skip specific tags, since already counted in TAG_NONE
-                if (tag[i] != TAG_NONE) continue;
+                if (tag[i] != TAG_NONE && !includeTags) continue;
 
                 entry.rxBytes += rxBytes[i];
                 entry.rxPackets += rxPackets[i];
@@ -460,7 +475,7 @@
      * time, and that none of them have disappeared.
      */
     public NetworkStats subtract(NetworkStats right) {
-        return subtract(this, right, null);
+        return subtract(this, right, null, null);
     }
 
     /**
@@ -471,12 +486,12 @@
      * If counters have rolled backwards, they are clamped to {@code 0} and
      * reported to the given {@link NonMonotonicObserver}.
      */
-    public static NetworkStats subtract(
-            NetworkStats left, NetworkStats right, NonMonotonicObserver observer) {
+    public static <C> NetworkStats subtract(
+            NetworkStats left, NetworkStats right, NonMonotonicObserver<C> observer, C cookie) {
         long deltaRealtime = left.elapsedRealtime - right.elapsedRealtime;
         if (deltaRealtime < 0) {
             if (observer != null) {
-                observer.foundNonMonotonic(left, -1, right, -1);
+                observer.foundNonMonotonic(left, -1, right, -1, cookie);
             }
             deltaRealtime = 0;
         }
@@ -510,7 +525,7 @@
                 if (entry.rxBytes < 0 || entry.rxPackets < 0 || entry.txBytes < 0
                         || entry.txPackets < 0 || entry.operations < 0) {
                     if (observer != null) {
-                        observer.foundNonMonotonic(left, i, right, j);
+                        observer.foundNonMonotonic(left, i, right, j, cookie);
                     }
                     entry.rxBytes = Math.max(entry.rxBytes, 0);
                     entry.rxPackets = Math.max(entry.rxPackets, 0);
@@ -663,8 +678,8 @@
         }
     };
 
-    public interface NonMonotonicObserver {
+    public interface NonMonotonicObserver<C> {
         public void foundNonMonotonic(
-                NetworkStats left, int leftIndex, NetworkStats right, int rightIndex);
+                NetworkStats left, int leftIndex, NetworkStats right, int rightIndex, C cookie);
     }
 }
diff --git a/core/java/android/net/NetworkStatsHistory.java b/core/java/android/net/NetworkStatsHistory.java
index 8c01331..faf8a3f 100644
--- a/core/java/android/net/NetworkStatsHistory.java
+++ b/core/java/android/net/NetworkStatsHistory.java
@@ -26,16 +26,18 @@
 import static android.net.NetworkStatsHistory.Entry.UNKNOWN;
 import static android.net.NetworkStatsHistory.ParcelUtils.readLongArray;
 import static android.net.NetworkStatsHistory.ParcelUtils.writeLongArray;
+import static com.android.internal.util.ArrayUtils.total;
 
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.MathUtils;
 
+import com.android.internal.util.IndentingPrintWriter;
+
 import java.io.CharArrayWriter;
 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;
@@ -74,6 +76,7 @@
     private long[] txBytes;
     private long[] txPackets;
     private long[] operations;
+    private long totalBytes;
 
     public static class Entry {
         public static final long UNKNOWN = -1;
@@ -106,6 +109,12 @@
         if ((fields & FIELD_TX_PACKETS) != 0) txPackets = new long[initialSize];
         if ((fields & FIELD_OPERATIONS) != 0) operations = new long[initialSize];
         bucketCount = 0;
+        totalBytes = 0;
+    }
+
+    public NetworkStatsHistory(NetworkStatsHistory existing, long bucketDuration) {
+        this(bucketDuration, existing.estimateResizeBuckets(bucketDuration));
+        recordEntireHistory(existing);
     }
 
     public NetworkStatsHistory(Parcel in) {
@@ -118,6 +127,7 @@
         txPackets = readLongArray(in);
         operations = readLongArray(in);
         bucketCount = bucketStart.length;
+        totalBytes = in.readLong();
     }
 
     /** {@inheritDoc} */
@@ -130,6 +140,7 @@
         writeLongArray(out, txBytes, bucketCount);
         writeLongArray(out, txPackets, bucketCount);
         writeLongArray(out, operations, bucketCount);
+        out.writeLong(totalBytes);
     }
 
     public NetworkStatsHistory(DataInputStream in) throws IOException {
@@ -144,6 +155,7 @@
                 txPackets = new long[bucketStart.length];
                 operations = new long[bucketStart.length];
                 bucketCount = bucketStart.length;
+                totalBytes = total(rxBytes) + total(txBytes);
                 break;
             }
             case VERSION_ADD_PACKETS:
@@ -158,6 +170,7 @@
                 txPackets = readVarLongArray(in);
                 operations = readVarLongArray(in);
                 bucketCount = bucketStart.length;
+                totalBytes = total(rxBytes) + total(txBytes);
                 break;
             }
             default: {
@@ -208,6 +221,13 @@
     }
 
     /**
+     * Return total bytes represented by this history.
+     */
+    public long getTotalBytes() {
+        return totalBytes;
+    }
+
+    /**
      * Return index of bucket that contains or is immediately before the
      * requested time.
      */
@@ -266,13 +286,16 @@
      * distribute across internal buckets, creating new buckets as needed.
      */
     public void recordData(long start, long end, NetworkStats.Entry entry) {
-        if (entry.rxBytes < 0 || entry.rxPackets < 0 || entry.txBytes < 0 || entry.txPackets < 0
-                || entry.operations < 0) {
+        long rxBytes = entry.rxBytes;
+        long rxPackets = entry.rxPackets;
+        long txBytes = entry.txBytes;
+        long txPackets = entry.txPackets;
+        long operations = entry.operations;
+
+        if (entry.isNegative()) {
             throw new IllegalArgumentException("tried recording negative data");
         }
-        if (entry.rxBytes == 0 && entry.rxPackets == 0 && entry.txBytes == 0 && entry.txPackets == 0
-                && entry.operations == 0) {
-            // nothing to record; skip
+        if (entry.isEmpty()) {
             return;
         }
 
@@ -295,21 +318,23 @@
             if (overlap <= 0) continue;
 
             // integer math each time is faster than floating point
-            final long fracRxBytes = entry.rxBytes * overlap / duration;
-            final long fracRxPackets = entry.rxPackets * overlap / duration;
-            final long fracTxBytes = entry.txBytes * overlap / duration;
-            final long fracTxPackets = entry.txPackets * overlap / duration;
-            final long fracOperations = entry.operations * overlap / duration;
+            final long fracRxBytes = rxBytes * overlap / duration;
+            final long fracRxPackets = rxPackets * overlap / duration;
+            final long fracTxBytes = txBytes * overlap / duration;
+            final long fracTxPackets = txPackets * overlap / duration;
+            final long fracOperations = operations * overlap / duration;
 
             addLong(activeTime, i, overlap);
-            addLong(rxBytes, i, fracRxBytes); entry.rxBytes -= fracRxBytes;
-            addLong(rxPackets, i, fracRxPackets); entry.rxPackets -= fracRxPackets;
-            addLong(txBytes, i, fracTxBytes); entry.txBytes -= fracTxBytes;
-            addLong(txPackets, i, fracTxPackets); entry.txPackets -= fracTxPackets;
-            addLong(operations, i, fracOperations); entry.operations -= fracOperations;
+            addLong(this.rxBytes, i, fracRxBytes); rxBytes -= fracRxBytes;
+            addLong(this.rxPackets, i, fracRxPackets); rxPackets -= fracRxPackets;
+            addLong(this.txBytes, i, fracTxBytes); txBytes -= fracTxBytes;
+            addLong(this.txPackets, i, fracTxPackets); txPackets -= fracTxPackets;
+            addLong(this.operations, i, fracOperations); operations -= fracOperations;
 
             duration -= overlap;
         }
+
+        totalBytes += entry.rxBytes + entry.txBytes;
     }
 
     /**
@@ -394,6 +419,7 @@
     /**
      * Remove buckets older than requested cutoff.
      */
+    @Deprecated
     public void removeBucketsBefore(long cutoff) {
         int i;
         for (i = 0; i < bucketCount; i++) {
@@ -415,6 +441,8 @@
             if (txPackets != null) txPackets = Arrays.copyOfRange(txPackets, i, length);
             if (operations != null) operations = Arrays.copyOfRange(operations, i, length);
             bucketCount -= i;
+
+            // TODO: subtract removed values from totalBytes
         }
     }
 
@@ -527,19 +555,17 @@
         return (long) (start + (r.nextFloat() * (end - start)));
     }
 
-    public void dump(String prefix, PrintWriter pw, boolean fullHistory) {
-        pw.print(prefix);
+    public void dump(IndentingPrintWriter pw, boolean fullHistory) {
         pw.print("NetworkStatsHistory: bucketDuration="); pw.println(bucketDuration);
+        pw.increaseIndent();
 
         final int start = fullHistory ? 0 : Math.max(0, bucketCount - 32);
         if (start > 0) {
-            pw.print(prefix);
-            pw.print("  (omitting "); pw.print(start); pw.println(" buckets)");
+            pw.print("(omitting "); pw.print(start); pw.println(" buckets)");
         }
 
         for (int i = start; i < bucketCount; i++) {
-            pw.print(prefix);
-            pw.print("  bucketStart="); pw.print(bucketStart[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]); }
@@ -548,12 +574,14 @@
             if (operations != null) { pw.print(" operations="); pw.print(operations[i]); }
             pw.println();
         }
+
+        pw.decreaseIndent();
     }
 
     @Override
     public String toString() {
         final CharArrayWriter writer = new CharArrayWriter();
-        dump("", new PrintWriter(writer), false);
+        dump(new IndentingPrintWriter(writer, "  "), false);
         return writer.toString();
     }
 
@@ -579,6 +607,10 @@
         if (array != null) array[i] += value;
     }
 
+    public int estimateResizeBuckets(long newBucketDuration) {
+        return (int) (size() * getBucketDuration() / newBucketDuration);
+    }
+
     /**
      * Utility methods for interacting with {@link DataInputStream} and
      * {@link DataOutputStream}, mostly dealing with writing partial arrays.
diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java
index 8bdb669..dfdea38 100644
--- a/core/java/android/net/TrafficStats.java
+++ b/core/java/android/net/TrafficStats.java
@@ -195,7 +195,7 @@
             // subtract starting values and return delta
             final NetworkStats profilingStop = getDataLayerSnapshotForUid(context);
             final NetworkStats profilingDelta = NetworkStats.subtract(
-                    profilingStop, sActiveProfilingStart, null);
+                    profilingStop, sActiveProfilingStart, null, null);
             sActiveProfilingStart = null;
             return profilingDelta;
         }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index ba9046c..ef8cb16 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -4111,17 +4111,38 @@
         /** {@hide} */
         public static final String NETSTATS_POLL_INTERVAL = "netstats_poll_interval";
         /** {@hide} */
-        public static final String NETSTATS_PERSIST_THRESHOLD = "netstats_persist_threshold";
+        public static final String NETSTATS_TIME_CACHE_MAX_AGE = "netstats_time_cache_max_age";
         /** {@hide} */
-        public static final String NETSTATS_NETWORK_BUCKET_DURATION = "netstats_network_bucket_duration";
+        public static final String NETSTATS_GLOBAL_ALERT_BYTES = "netstats_global_alert_bytes";
         /** {@hide} */
-        public static final String NETSTATS_NETWORK_MAX_HISTORY = "netstats_network_max_history";
+        public static final String NETSTATS_SAMPLE_ENABLED = "netstats_sample_enabled";
+
+        /** {@hide} */
+        public static final String NETSTATS_DEV_BUCKET_DURATION = "netstats_dev_bucket_duration";
+        /** {@hide} */
+        public static final String NETSTATS_DEV_PERSIST_BYTES = "netstats_dev_persist_bytes";
+        /** {@hide} */
+        public static final String NETSTATS_DEV_ROTATE_AGE = "netstats_dev_rotate_age";
+        /** {@hide} */
+        public static final String NETSTATS_DEV_DELETE_AGE = "netstats_dev_delete_age";
+
         /** {@hide} */
         public static final String NETSTATS_UID_BUCKET_DURATION = "netstats_uid_bucket_duration";
         /** {@hide} */
-        public static final String NETSTATS_UID_MAX_HISTORY = "netstats_uid_max_history";
+        public static final String NETSTATS_UID_PERSIST_BYTES = "netstats_uid_persist_bytes";
         /** {@hide} */
-        public static final String NETSTATS_TAG_MAX_HISTORY = "netstats_tag_max_history";
+        public static final String NETSTATS_UID_ROTATE_AGE = "netstats_uid_rotate_age";
+        /** {@hide} */
+        public static final String NETSTATS_UID_DELETE_AGE = "netstats_uid_delete_age";
+
+        /** {@hide} */
+        public static final String NETSTATS_UID_TAG_BUCKET_DURATION = "netstats_uid_tag_bucket_duration";
+        /** {@hide} */
+        public static final String NETSTATS_UID_TAG_PERSIST_BYTES = "netstats_uid_tag_persist_bytes";
+        /** {@hide} */
+        public static final String NETSTATS_UID_TAG_ROTATE_AGE = "netstats_uid_tag_rotate_age";
+        /** {@hide} */
+        public static final String NETSTATS_UID_TAG_DELETE_AGE = "netstats_uid_tag_delete_age";
 
         /** Preferred NTP server. {@hide} */
         public static final String NTP_SERVER = "ntp_server";
diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java
index c184c11..a52e2ba 100644
--- a/core/java/android/text/MeasuredText.java
+++ b/core/java/android/text/MeasuredText.java
@@ -109,6 +109,9 @@
             for (int i = 0; i < spans.length; i++) {
                 int startInPara = spanned.getSpanStart(spans[i]) - start;
                 int endInPara = spanned.getSpanEnd(spans[i]) - start;
+                // The span interval may be larger and must be restricted to [start, end[
+                if (startInPara < 0) startInPara = 0;
+                if (endInPara > len) endInPara = len;
                 for (int j = startInPara; j < endInPara; j++) {
                     mChars[j] = '\uFFFC';
                 }
diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java
index 5ec1ec3..bd02d62 100644
--- a/core/java/android/view/inputmethod/BaseInputConnection.java
+++ b/core/java/android/view/inputmethod/BaseInputConnection.java
@@ -193,10 +193,12 @@
     /**
      * The default implementation performs the deletion around the current
      * selection position of the editable text.
+     * @param beforeLength
+     * @param afterLength
      */
-    public boolean deleteSurroundingText(int leftLength, int rightLength) {
-        if (DEBUG) Log.v(TAG, "deleteSurroundingText " + leftLength
-                + " / " + rightLength);
+    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
+        if (DEBUG) Log.v(TAG, "deleteSurroundingText " + beforeLength
+                + " / " + afterLength);
         final Editable content = getEditable();
         if (content == null) return false;
 
@@ -226,17 +228,17 @@
 
         int deleted = 0;
 
-        if (leftLength > 0) {
-            int start = a - leftLength;
+        if (beforeLength > 0) {
+            int start = a - beforeLength;
             if (start < 0) start = 0;
             content.delete(start, a);
             deleted = a - start;
         }
 
-        if (rightLength > 0) {
+        if (afterLength > 0) {
             b = b - deleted;
 
-            int end = b + rightLength;
+            int end = b + afterLength;
             if (end > content.length()) end = content.length();
 
             content.delete(b, end);
diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java
index a6639d1..bc2a270 100644
--- a/core/java/android/view/inputmethod/InputConnection.java
+++ b/core/java/android/view/inputmethod/InputConnection.java
@@ -138,19 +138,20 @@
             int flags);
 
     /**
-     * Delete <var>leftLength</var> characters of text before the current cursor
-     * position, and delete <var>rightLength</var> characters of text after the
+     * Delete <var>beforeLength</var> characters of text before the current cursor
+     * position, and delete <var>afterLength</var> characters of text after the
      * current cursor position, excluding composing text.
      * 
-     * @param leftLength The number of characters to be deleted before the
+     *
+     * @param beforeLength The number of characters to be deleted before the
      *        current cursor position.
-     * @param rightLength The number of characters to be deleted after the
+     * @param afterLength The number of characters to be deleted after the
      *        current cursor position.
-     *        
+     *
      * @return Returns true on success, false if the input connection is no longer
      * valid.
      */
-    public boolean deleteSurroundingText(int leftLength, int rightLength);
+    public boolean deleteSurroundingText(int beforeLength, int afterLength);
 
     /**
      * Set composing text around the current cursor position with the given text,
diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java
index 690ea85..a48473e 100644
--- a/core/java/android/view/inputmethod/InputConnectionWrapper.java
+++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java
@@ -62,8 +62,8 @@
         return mTarget.getExtractedText(request, flags);
     }
 
-    public boolean deleteSurroundingText(int leftLength, int rightLength) {
-        return mTarget.deleteSurroundingText(leftLength, rightLength);
+    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
+        return mTarget.deleteSurroundingText(beforeLength, afterLength);
     }
 
     public boolean setComposingText(CharSequence text, int newCursorPosition) {
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index 148be5c..3697635 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -384,17 +384,17 @@
         }
 
         @Override
-        public boolean deleteSurroundingText(int leftLength, int rightLength) {
+        public boolean deleteSurroundingText(int beforeLength, int afterLength) {
             // Look for one-character delete and send it as a key press.
-            if (leftLength == 1 && rightLength == 0) {
+            if (beforeLength == 1 && afterLength == 0) {
                 sendKeyPress(KeyEvent.KEYCODE_DEL);
-            } else if (leftLength == 0 && rightLength == 1){
+            } else if (beforeLength == 0 && afterLength == 1){
                 sendKeyPress(KeyEvent.KEYCODE_FORWARD_DEL);
             } else if (mWebViewCore != null) {
                 mWebViewCore.sendMessage(EventHub.DELETE_SURROUNDING_TEXT,
-                        leftLength, rightLength);
+                        beforeLength, afterLength);
             }
-            return super.deleteSurroundingText(leftLength, rightLength);
+            return super.deleteSurroundingText(beforeLength, afterLength);
         }
     }
 
diff --git a/core/java/android/widget/ActivityChooserView.java b/core/java/android/widget/ActivityChooserView.java
index 60b24bc..be6b4e2 100644
--- a/core/java/android/widget/ActivityChooserView.java
+++ b/core/java/android/widget/ActivityChooserView.java
@@ -33,8 +33,6 @@
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityManager;
 import android.widget.ActivityChooserModel.ActivityChooserModelClient;
 
 /**
@@ -366,7 +364,7 @@
             getListPopupWindow().dismiss();
             ViewTreeObserver viewTreeObserver = getViewTreeObserver();
             if (viewTreeObserver.isAlive()) {
-                viewTreeObserver.removeGlobalOnLayoutListener(mOnGlobalLayoutListener);
+                viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
             }
         }
         return true;
@@ -400,7 +398,7 @@
         }
         ViewTreeObserver viewTreeObserver = getViewTreeObserver();
         if (viewTreeObserver.isAlive()) {
-            viewTreeObserver.removeGlobalOnLayoutListener(mOnGlobalLayoutListener);
+            viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
         }
         mIsAttachedToWindow = false;
     }
@@ -547,6 +545,7 @@
                         position = mAdapter.getShowDefaultActivity() ? position : position + 1;
                         Intent launchIntent = mAdapter.getDataModel().chooseActivity(position);
                         if (launchIntent != null) {
+                            launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
                             mContext.startActivity(launchIntent);
                         }
                     }
@@ -564,6 +563,7 @@
                 final int index = mAdapter.getDataModel().getActivityIndex(defaultActivity);
                 Intent launchIntent = mAdapter.getDataModel().chooseActivity(index);
                 if (launchIntent != null) {
+                    launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
                     mContext.startActivity(launchIntent);
                 }
             } else if (view == mExpandActivityOverflowButton) {
diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java
index e226d37..bb00049 100644
--- a/core/java/android/widget/AdapterViewAnimator.java
+++ b/core/java/android/widget/AdapterViewAnimator.java
@@ -558,7 +558,9 @@
             mCurrentWindowEnd = newWindowEnd;
             mCurrentWindowStartUnbounded = newWindowStartUnbounded;
             if (mRemoteViewsAdapter != null) {
-                mRemoteViewsAdapter.setVisibleRangeHint(mCurrentWindowStart, mCurrentWindowEnd);
+                int adapterStart = modulo(mCurrentWindowStart, adapterCount);
+                int adapterEnd = modulo(mCurrentWindowEnd, adapterCount);
+                mRemoteViewsAdapter.setVisibleRangeHint(adapterStart, adapterEnd);
             }
         }
         requestLayout();
diff --git a/core/java/android/widget/ShareActionProvider.java b/core/java/android/widget/ShareActionProvider.java
index bb27b73..22e9ef1 100644
--- a/core/java/android/widget/ShareActionProvider.java
+++ b/core/java/android/widget/ShareActionProvider.java
@@ -279,6 +279,7 @@
             final int itemId = item.getItemId();
             Intent launchIntent = dataModel.chooseActivity(itemId);
             if (launchIntent != null) {
+                launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
                 mContext.startActivity(launchIntent);
             }
             return true;
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 04f4a83..9fb26ae 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -8386,10 +8386,6 @@
             }
 
             if (touchIsFinished && (isTextEditable() || mTextIsSelectable)) {
-                // Move cursor
-                final int offset = getOffsetForPosition(event.getX(), event.getY());
-                Selection.setSelection((Spannable) mText, offset);
-
                 // Show the IME, except when selecting in read-only text.
                 final InputMethodManager imm = InputMethodManager.peekInstance();
                 viewClicked(imm);
@@ -8416,6 +8412,9 @@
                             postDelayed(mShowSuggestionRunnable,
                                     ViewConfiguration.getDoubleTapTimeout());
                         } else if (hasInsertionController()) {
+                            // Move cursor
+                            final int offset = getOffsetForPosition(event.getX(), event.getY());
+                            Selection.setSelection((Spannable) mText, offset);
                             getInsertionController().show();
                         }
                     }
diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java
index edeb2a8..d1aa1ce 100644
--- a/core/java/com/android/internal/util/ArrayUtils.java
+++ b/core/java/com/android/internal/util/ArrayUtils.java
@@ -142,6 +142,14 @@
         return false;
     }
 
+    public static long total(long[] array) {
+        long total = 0;
+        for (long value : array) {
+            total += value;
+        }
+        return total;
+    }
+
     /**
      * Appends an element to a copy of the array and returns the copy.
      * @param array The original array, or null to represent an empty array.
diff --git a/core/java/com/android/internal/util/FileRotator.java b/core/java/com/android/internal/util/FileRotator.java
index 3ce95e7..8a8f315 100644
--- a/core/java/com/android/internal/util/FileRotator.java
+++ b/core/java/com/android/internal/util/FileRotator.java
@@ -17,9 +17,9 @@
 package com.android.internal.util;
 
 import android.os.FileUtils;
+import android.util.Slog;
 
-import com.android.internal.util.FileRotator.Reader;
-import com.android.internal.util.FileRotator.Writer;
+import com.android.internal.util.FileRotator.Rewriter;
 
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
@@ -41,12 +41,15 @@
  * Instead of manipulating files directly, users implement interfaces that
  * perform operations on {@link InputStream} and {@link OutputStream}. This
  * enables atomic rewriting of file contents in
- * {@link #combineActive(Reader, Writer, long)}.
+ * {@link #rewriteActive(Rewriter, long)}.
  * <p>
  * Users must periodically call {@link #maybeRotate(long)} to perform actual
  * rotation. Not inherently thread safe.
  */
 public class FileRotator {
+    private static final String TAG = "FileRotator";
+    private static final boolean LOGD = true;
+
     private final File mBasePath;
     private final String mPrefix;
     private final long mRotateAgeMillis;
@@ -73,6 +76,15 @@
     }
 
     /**
+     * External class that reads existing data from given {@link InputStream},
+     * then writes any modified data to {@link OutputStream}.
+     */
+    public interface Rewriter extends Reader, Writer {
+        public void reset();
+        public boolean shouldWrite();
+    }
+
+    /**
      * Create a file rotator.
      *
      * @param basePath Directory under which all files will be placed.
@@ -96,6 +108,8 @@
             if (!name.startsWith(mPrefix)) continue;
 
             if (name.endsWith(SUFFIX_BACKUP)) {
+                if (LOGD) Slog.d(TAG, "recovering " + name);
+
                 final File backupFile = new File(mBasePath, name);
                 final File file = new File(
                         mBasePath, name.substring(0, name.length() - SUFFIX_BACKUP.length()));
@@ -104,6 +118,8 @@
                 backupFile.renameTo(file);
 
             } else if (name.endsWith(SUFFIX_NO_BACKUP)) {
+                if (LOGD) Slog.d(TAG, "recovering " + name);
+
                 final File noBackupFile = new File(mBasePath, name);
                 final File file = new File(
                         mBasePath, name.substring(0, name.length() - SUFFIX_NO_BACKUP.length()));
@@ -116,26 +132,95 @@
     }
 
     /**
-     * Atomically combine data with existing data in currently active file.
-     * Maintains a backup during write, which is restored if the write fails.
+     * Delete all files managed by this rotator.
      */
-    public void combineActive(Reader reader, Writer writer, long currentTimeMillis)
+    public void deleteAll() {
+        final FileInfo info = new FileInfo(mPrefix);
+        for (String name : mBasePath.list()) {
+            if (!info.parse(name)) continue;
+
+            // delete each file that matches parser
+            new File(mBasePath, name).delete();
+        }
+    }
+
+    /**
+     * Process currently active file, first reading any existing data, then
+     * writing modified data. Maintains a backup during write, which is restored
+     * if the write fails.
+     */
+    public void rewriteActive(Rewriter rewriter, long currentTimeMillis)
             throws IOException {
         final String activeName = getActiveName(currentTimeMillis);
+        rewriteSingle(rewriter, activeName);
+    }
 
-        final File file = new File(mBasePath, activeName);
+    @Deprecated
+    public void combineActive(final Reader reader, final Writer writer, long currentTimeMillis)
+            throws IOException {
+        rewriteActive(new Rewriter() {
+            /** {@inheritDoc} */
+            public void reset() {
+                // ignored
+            }
+
+            /** {@inheritDoc} */
+            public void read(InputStream in) throws IOException {
+                reader.read(in);
+            }
+
+            /** {@inheritDoc} */
+            public boolean shouldWrite() {
+                return true;
+            }
+
+            /** {@inheritDoc} */
+            public void write(OutputStream out) throws IOException {
+                writer.write(out);
+            }
+        }, currentTimeMillis);
+    }
+
+    /**
+     * Process all files managed by this rotator, usually to rewrite historical
+     * data. Each file is processed atomically.
+     */
+    public void rewriteAll(Rewriter rewriter) throws IOException {
+        final FileInfo info = new FileInfo(mPrefix);
+        for (String name : mBasePath.list()) {
+            if (!info.parse(name)) continue;
+
+            // process each file that matches parser
+            rewriteSingle(rewriter, name);
+        }
+    }
+
+    /**
+     * Process a single file atomically, first reading any existing data, then
+     * writing modified data. Maintains a backup during write, which is restored
+     * if the write fails.
+     */
+    private void rewriteSingle(Rewriter rewriter, String name) throws IOException {
+        if (LOGD) Slog.d(TAG, "rewriting " + name);
+
+        final File file = new File(mBasePath, name);
         final File backupFile;
 
+        rewriter.reset();
+
         if (file.exists()) {
             // read existing data
-            readFile(file, reader);
+            readFile(file, rewriter);
+
+            // skip when rewriter has nothing to write
+            if (!rewriter.shouldWrite()) return;
 
             // backup existing data during write
-            backupFile = new File(mBasePath, activeName + SUFFIX_BACKUP);
+            backupFile = new File(mBasePath, name + SUFFIX_BACKUP);
             file.renameTo(backupFile);
 
             try {
-                writeFile(file, writer);
+                writeFile(file, rewriter);
 
                 // write success, delete backup
                 backupFile.delete();
@@ -148,11 +233,11 @@
 
         } else {
             // create empty backup during write
-            backupFile = new File(mBasePath, activeName + SUFFIX_NO_BACKUP);
+            backupFile = new File(mBasePath, name + SUFFIX_NO_BACKUP);
             backupFile.createNewFile();
 
             try {
-                writeFile(file, writer);
+                writeFile(file, rewriter);
 
                 // write success, delete empty backup
                 backupFile.delete();
@@ -176,6 +261,8 @@
 
             // read file when it overlaps
             if (info.startMillis <= matchEndMillis && matchStartMillis <= info.endMillis) {
+                if (LOGD) Slog.d(TAG, "reading matching " + name);
+
                 final File file = new File(mBasePath, name);
                 readFile(file, reader);
             }
@@ -224,16 +311,20 @@
             if (!info.parse(name)) continue;
 
             if (info.isActive()) {
-                // found active file; rotate if old enough
-                if (info.startMillis < rotateBefore) {
+                if (info.startMillis <= rotateBefore) {
+                    // found active file; rotate if old enough
+                    if (LOGD) Slog.d(TAG, "rotating " + name);
+
                     info.endMillis = currentTimeMillis;
 
                     final File file = new File(mBasePath, name);
                     final File destFile = new File(mBasePath, info.build());
                     file.renameTo(destFile);
                 }
-            } else if (info.endMillis < deleteBefore) {
+            } else if (info.endMillis <= deleteBefore) {
                 // found rotated file; delete if old enough
+                if (LOGD) Slog.d(TAG, "deleting " + name);
+
                 final File file = new File(mBasePath, name);
                 file.delete();
             }
diff --git a/core/java/com/android/internal/util/IndentingPrintWriter.java b/core/java/com/android/internal/util/IndentingPrintWriter.java
new file mode 100644
index 0000000..3dd2284
--- /dev/null
+++ b/core/java/com/android/internal/util/IndentingPrintWriter.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2012 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.internal.util;
+
+import java.io.PrintWriter;
+import java.io.Writer;
+
+/**
+ * Lightweight wrapper around {@link PrintWriter} that automatically indents
+ * newlines based on internal state. Delays writing indent until first actual
+ * write on a newline, enabling indent modification after newline.
+ */
+public class IndentingPrintWriter extends PrintWriter {
+    private final String mIndent;
+
+    private StringBuilder mBuilder = new StringBuilder();
+    private String mCurrent = new String();
+    private boolean mEmptyLine = true;
+
+    public IndentingPrintWriter(Writer writer, String indent) {
+        super(writer);
+        mIndent = indent;
+    }
+
+    public void increaseIndent() {
+        mBuilder.append(mIndent);
+        mCurrent = mBuilder.toString();
+    }
+
+    public void decreaseIndent() {
+        mBuilder.delete(0, mIndent.length());
+        mCurrent = mBuilder.toString();
+    }
+
+    @Override
+    public void println() {
+        super.println();
+        mEmptyLine = true;
+    }
+
+    @Override
+    public void write(char[] buf, int offset, int count) {
+        if (mEmptyLine) {
+            mEmptyLine = false;
+            super.print(mCurrent);
+        }
+        super.write(buf, offset, count);
+    }
+}
diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java
index a235d9a..9024d8d 100644
--- a/core/java/com/android/internal/view/InputConnectionWrapper.java
+++ b/core/java/com/android/internal/view/InputConnectionWrapper.java
@@ -387,9 +387,9 @@
         }
     }
     
-    public boolean deleteSurroundingText(int leftLength, int rightLength) {
+    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
         try {
-            mIInputContext.deleteSurroundingText(leftLength, rightLength);
+            mIInputContext.deleteSurroundingText(beforeLength, afterLength);
             return true;
         } catch (RemoteException e) {
             return false;
diff --git a/docs/html/guide/developing/device.jd b/docs/html/guide/developing/device.jd
index c4d08ed..e46d07c 100644
--- a/docs/html/guide/developing/device.jd
+++ b/docs/html/guide/developing/device.jd
@@ -154,6 +154,14 @@
     <td><code>0489</code></td>
   </tr>
   <tr>
+    <td>Fujitsu</td>
+    <td><code>04C5</code></td>
+  </tr>
+  <tr>
+    <td>Fujitsu Toshiba</td>
+    <td><code>04C5</code></td>
+  </tr>
+  <tr>
     <td>Garmin-Asus</td>
     <td><code>091E</code></td>
   </tr>
diff --git a/media/libstagefright/codecs/aacenc/src/adj_thr.c b/media/libstagefright/codecs/aacenc/src/adj_thr.c
index c656f65..a8ab809 100644
--- a/media/libstagefright/codecs/aacenc/src/adj_thr.c
+++ b/media/libstagefright/codecs/aacenc/src/adj_thr.c
@@ -1039,7 +1039,7 @@
 
   /* minSnr adaptation */
   /* maximum reduction of minSnr goes down to minSnr^maxRed */
-  msaParam->maxRed = 0x20000000;     /* *0.25f /
+  msaParam->maxRed = 0x20000000;     /* *0.25f */
   /* start adaptation of minSnr for avgEn/sfbEn > startRatio */
   msaParam->startRatio = 0x0ccccccd; /* 10 */
   /* maximum minSnr reduction to minSnr^maxRed is reached for
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 69a247d..d33ed3f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -23,6 +23,8 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
 import android.os.ServiceManager;
 import android.util.AttributeSet;
 import android.util.Slog;
@@ -66,6 +68,35 @@
     int mDisabledFlags = 0;
     int mNavigationIconHints = 0;
 
+    // workaround for LayoutTransitions leaving the nav buttons in a weird state (bug 5549288)
+    final static boolean WORKAROUND_INVALID_LAYOUT = true;
+    final static int MSG_CHECK_INVALID_LAYOUT = 8686;
+
+    private class H extends Handler {
+        public void handleMessage(Message m) {
+            switch (m.what) {
+                case MSG_CHECK_INVALID_LAYOUT:
+                    final String how = "" + m.obj;
+                    final int w = getWidth();
+                    final int h = getHeight();
+                    final int vw = mCurrentView.getWidth();
+                    final int vh = mCurrentView.getHeight();
+
+                    if (h != vh || w != vw) {
+                        Slog.w(TAG, String.format(
+                            "*** Invalid layout in navigation bar (%s this=%dx%d cur=%dx%d)",
+                            how, w, h, vw, vh));
+                        if (WORKAROUND_INVALID_LAYOUT) {
+                            requestLayout();
+                        }
+                    }
+                    break;
+            }
+        }
+    }
+
+    private H mHandler = new H();
+
     public View getRecentsButton() {
         return mCurrentView.findViewById(R.id.recent_apps);
     }
@@ -275,6 +306,36 @@
         }
     }
 
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        if (DEBUG) Slog.d(TAG, String.format(
+                    "onSizeChanged: (%dx%d) old: (%dx%d)", w, h, oldw, oldh));
+        postCheckForInvalidLayout("sizeChanged");
+        super.onSizeChanged(w, h, oldw, oldh);
+    }
+
+    /*
+    @Override
+    protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
+        if (DEBUG) Slog.d(TAG, String.format(
+                    "onLayout: %s (%d,%d,%d,%d)", 
+                    changed?"changed":"notchanged", left, top, right, bottom));
+        super.onLayout(changed, left, top, right, bottom);
+    }
+
+    // uncomment this for extra defensiveness in WORKAROUND_INVALID_LAYOUT situations: if all else
+    // fails, any touch on the display will fix the layout.
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (DEBUG) Slog.d(TAG, "onInterceptTouchEvent: " + ev.toString());
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            postCheckForInvalidLayout("touch");
+        }
+        return super.onInterceptTouchEvent(ev);
+    }
+    */
+        
+
     private String getResourceName(int resId) {
         if (resId != 0) {
             final android.content.res.Resources res = mContext.getResources();
@@ -288,6 +349,10 @@
         }
     }
 
+    private void postCheckForInvalidLayout(final String how) {
+        mHandler.obtainMessage(MSG_CHECK_INVALID_LAYOUT, 0, 0, how).sendToTarget();
+    }
+
     private static String visibilityToString(int vis) {
         switch (vis) {
             case View.INVISIBLE:
diff --git a/packages/VpnDialogs/AndroidManifest.xml b/packages/VpnDialogs/AndroidManifest.xml
index 8e062b7..46c0f83 100644
--- a/packages/VpnDialogs/AndroidManifest.xml
+++ b/packages/VpnDialogs/AndroidManifest.xml
@@ -5,7 +5,7 @@
     <application android:label="VpnDialogs"
             android:allowBackup="false" >
         <activity android:name=".ConfirmDialog"
-                android:theme="@style/transparent">
+                android:theme="@*android:style/Theme.Holo.Dialog.Alert">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.DEFAULT"/>
@@ -13,7 +13,7 @@
         </activity>
 
         <activity android:name=".ManageDialog"
-                android:theme="@style/transparent"
+                android:theme="@*android:style/Theme.Holo.Dialog.Alert"
                 android:noHistory="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
diff --git a/packages/VpnDialogs/res/values/styles.xml b/packages/VpnDialogs/res/values/styles.xml
index cf10596..e3469ec 100644
--- a/packages/VpnDialogs/res/values/styles.xml
+++ b/packages/VpnDialogs/res/values/styles.xml
@@ -15,13 +15,6 @@
 -->
 
 <resources>
-
-    <style name="transparent">
-        <item name="android:windowBackground">@android:color/transparent</item>
-        <item name="android:windowNoTitle">true</item>
-        <item name="android:windowIsFloating">true</item>
-    </style>
-
     <style name="label">
         <item name="android:gravity">center_vertical|right</item>
         <item name="android:paddingRight">2mm</item>
diff --git a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
index c7b4a5f..13d8019 100644
--- a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
+++ b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
@@ -16,8 +16,6 @@
 
 package com.android.vpndialogs;
 
-import android.app.Activity;
-import android.app.AlertDialog;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
@@ -32,15 +30,16 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
-public class ConfirmDialog extends Activity implements CompoundButton.OnCheckedChangeListener,
-        DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
+import com.android.internal.app.AlertActivity;
+
+public class ConfirmDialog extends AlertActivity implements
+        CompoundButton.OnCheckedChangeListener, DialogInterface.OnClickListener {
     private static final String TAG = "VpnConfirm";
 
     private String mPackage;
 
     private IConnectivityManager mService;
 
-    private AlertDialog mDialog;
     private Button mButton;
 
     @Override
@@ -67,18 +66,17 @@
                     getString(R.string.prompt, app.loadLabel(pm)));
             ((CompoundButton) view.findViewById(R.id.check)).setOnCheckedChangeListener(this);
 
-            mDialog = new AlertDialog.Builder(this)
-                    .setIcon(android.R.drawable.ic_dialog_alert)
-                    .setTitle(android.R.string.dialog_alert_title)
-                    .setView(view)
-                    .setPositiveButton(android.R.string.ok, this)
-                    .setNegativeButton(android.R.string.cancel, this)
-                    .setCancelable(false)
-                    .create();
-            mDialog.setOnDismissListener(this);
-            mDialog.show();
+            mAlertParams.mIconId = android.R.drawable.ic_dialog_alert;
+            mAlertParams.mTitle = getText(android.R.string.dialog_alert_title);
+            mAlertParams.mPositiveButtonText = getText(android.R.string.ok);
+            mAlertParams.mPositiveButtonListener = this;
+            mAlertParams.mNegativeButtonText = getText(android.R.string.cancel);
+            mAlertParams.mNegativeButtonListener = this;
+            mAlertParams.mView = view;
+            setupAlert();
 
-            mButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
+            getWindow().setCloseOnTouchOutside(false);
+            mButton = mAlert.getButton(DialogInterface.BUTTON_POSITIVE);
             mButton.setEnabled(false);
         } catch (Exception e) {
             Log.e(TAG, "onResume", e);
@@ -87,12 +85,7 @@
     }
 
     @Override
-    protected void onPause() {
-        super.onPause();
-        if (mDialog != null) {
-            mDialog.setOnDismissListener(null);
-            mDialog.dismiss();
-        }
+    public void onBackPressed() {
     }
 
     @Override
@@ -103,16 +96,11 @@
     @Override
     public void onClick(DialogInterface dialog, int which) {
         try {
-            if (which == AlertDialog.BUTTON_POSITIVE && mService.prepareVpn(null, mPackage)) {
+            if (which == DialogInterface.BUTTON_POSITIVE && mService.prepareVpn(null, mPackage)) {
                 setResult(RESULT_OK);
             }
         } catch (Exception e) {
             Log.e(TAG, "onClick", e);
         }
     }
-
-    @Override
-    public void onDismiss(DialogInterface dialog) {
-        finish();
-    }
 }
diff --git a/packages/VpnDialogs/src/com/android/vpndialogs/ManageDialog.java b/packages/VpnDialogs/src/com/android/vpndialogs/ManageDialog.java
index 7fb1417..2de0251 100644
--- a/packages/VpnDialogs/src/com/android/vpndialogs/ManageDialog.java
+++ b/packages/VpnDialogs/src/com/android/vpndialogs/ManageDialog.java
@@ -16,8 +16,6 @@
 
 package com.android.vpndialogs;
 
-import android.app.Activity;
-import android.app.AlertDialog;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
@@ -35,20 +33,20 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import com.android.internal.app.AlertActivity;
 import com.android.internal.net.VpnConfig;
 
 import java.io.DataInputStream;
 import java.io.FileInputStream;
 
-public class ManageDialog extends Activity implements Handler.Callback,
-        DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
+public class ManageDialog extends AlertActivity implements
+        DialogInterface.OnClickListener, Handler.Callback {
     private static final String TAG = "VpnManage";
 
     private VpnConfig mConfig;
 
     private IConnectivityManager mService;
 
-    private AlertDialog mDialog;
     private TextView mDuration;
     private TextView mDataTransmitted;
     private TextView mDataReceived;
@@ -80,31 +78,24 @@
             mDataReceived = (TextView) view.findViewById(R.id.data_received);
 
             if (mConfig.user.equals(VpnConfig.LEGACY_VPN)) {
-                mDialog = new AlertDialog.Builder(this)
-                        .setIcon(android.R.drawable.ic_dialog_info)
-                        .setTitle(R.string.legacy_title)
-                        .setView(view)
-                        .setNeutralButton(R.string.disconnect, this)
-                        .setNegativeButton(android.R.string.cancel, this)
-                        .create();
+                mAlertParams.mIconId = android.R.drawable.ic_dialog_info;
+                mAlertParams.mTitle = getText(R.string.legacy_title);
             } else {
                 PackageManager pm = getPackageManager();
                 ApplicationInfo app = pm.getApplicationInfo(mConfig.user, 0);
-                mDialog = new AlertDialog.Builder(this)
-                        .setIcon(app.loadIcon(pm))
-                        .setTitle(app.loadLabel(pm))
-                        .setView(view)
-                        .setNeutralButton(R.string.disconnect, this)
-                        .setNegativeButton(android.R.string.cancel, this)
-                        .create();
+                mAlertParams.mIcon = app.loadIcon(pm);
+                mAlertParams.mTitle = app.loadLabel(pm);
             }
-
             if (mConfig.configureIntent != null) {
-                mDialog.setButton(DialogInterface.BUTTON_POSITIVE,
-                        getText(R.string.configure), this);
+                mAlertParams.mPositiveButtonText = getText(R.string.configure);
+                mAlertParams.mPositiveButtonListener = this;
             }
-            mDialog.setOnDismissListener(this);
-            mDialog.show();
+            mAlertParams.mNeutralButtonText = getText(R.string.disconnect);
+            mAlertParams.mNeutralButtonListener = this;
+            mAlertParams.mNegativeButtonText = getText(android.R.string.cancel);
+            mAlertParams.mNegativeButtonListener = this;
+            mAlertParams.mView = view;
+            setupAlert();
 
             if (mHandler == null) {
                 mHandler = new Handler(this);
@@ -119,18 +110,17 @@
     @Override
     protected void onPause() {
         super.onPause();
-        if (mDialog != null) {
-            mDialog.setOnDismissListener(null);
-            mDialog.dismiss();
+        if (!isFinishing()) {
+            finish();
         }
     }
 
     @Override
     public void onClick(DialogInterface dialog, int which) {
         try {
-            if (which == AlertDialog.BUTTON_POSITIVE) {
+            if (which == DialogInterface.BUTTON_POSITIVE) {
                 mConfig.configureIntent.send();
-            } else if (which == AlertDialog.BUTTON_NEUTRAL) {
+            } else if (which == DialogInterface.BUTTON_NEUTRAL) {
                 mService.prepareVpn(mConfig.user, VpnConfig.LEGACY_VPN);
             }
         } catch (Exception e) {
@@ -140,15 +130,10 @@
     }
 
     @Override
-    public void onDismiss(DialogInterface dialog) {
-        finish();
-    }
-
-    @Override
     public boolean handleMessage(Message message) {
         mHandler.removeMessages(0);
 
-        if (mDialog.isShowing()) {
+        if (!isFinishing()) {
             if (mConfig.startTime != 0) {
                 long seconds = (SystemClock.elapsedRealtime() - mConfig.startTime) / 1000;
                 mDuration.setText(String.format("%02d:%02d:%02d",
diff --git a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
index 43a34d0..2d6e4f8 100755
--- a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
+++ b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
@@ -2044,6 +2044,12 @@
                         mTmpNavigationFrame.offset(mNavigationBarWidth, 0);
                     }
                 }
+                // Make sure the content and current rectangles are updated to
+                // account for the restrictions from the navigation bar.
+                mContentTop = mCurTop = mDockTop;
+                mContentBottom = mCurBottom = mDockBottom;
+                mContentLeft = mCurLeft = mDockLeft;
+                mContentRight = mCurRight = mDockRight;
                 // And compute the final frame.
                 mNavigationBar.computeFrameLw(mTmpNavigationFrame, mTmpNavigationFrame,
                         mTmpNavigationFrame, mTmpNavigationFrame);
diff --git a/services/java/com/android/server/EventLogTags.logtags b/services/java/com/android/server/EventLogTags.logtags
index 4dad209..0bcec2e 100644
--- a/services/java/com/android/server/EventLogTags.logtags
+++ b/services/java/com/android/server/EventLogTags.logtags
@@ -142,5 +142,5 @@
 # ---------------------------
 # NetworkStatsService.java
 # ---------------------------
-51100 netstats_mobile_sample (dev_rx_bytes|2|2),(dev_tx_bytes|2|2),(dev_rx_pkts|2|1),(dev_tx_pkts|2|1),(xt_rx_bytes|2|2),(xt_tx_bytes|2|2),(xt_rx_pkts|2|1),(xt_tx_pkts|2|1),(uid_rx_bytes|2|2),(uid_tx_bytes|2|2),(uid_rx_pkts|2|1),(uid_tx_pkts|2|1),(trusted_time|2|3),(dev_history_start|2|3)
-51101 netstats_wifi_sample (dev_rx_bytes|2|2),(dev_tx_bytes|2|2),(dev_rx_pkts|2|1),(dev_tx_pkts|2|1),(xt_rx_bytes|2|2),(xt_tx_bytes|2|2),(xt_rx_pkts|2|1),(xt_tx_pkts|2|1),(uid_rx_bytes|2|2),(uid_tx_bytes|2|2),(uid_rx_pkts|2|1),(uid_tx_pkts|2|1),(trusted_time|2|3),(dev_history_start|2|3)
+51100 netstats_mobile_sample (dev_rx_bytes|2|2),(dev_tx_bytes|2|2),(dev_rx_pkts|2|1),(dev_tx_pkts|2|1),(xt_rx_bytes|2|2),(xt_tx_bytes|2|2),(xt_rx_pkts|2|1),(xt_tx_pkts|2|1),(uid_rx_bytes|2|2),(uid_tx_bytes|2|2),(uid_rx_pkts|2|1),(uid_tx_pkts|2|1),(trusted_time|2|3)
+51101 netstats_wifi_sample (dev_rx_bytes|2|2),(dev_tx_bytes|2|2),(dev_rx_pkts|2|1),(dev_tx_pkts|2|1),(xt_rx_bytes|2|2),(xt_tx_bytes|2|2),(xt_rx_pkts|2|1),(xt_tx_pkts|2|1),(uid_rx_bytes|2|2),(uid_tx_bytes|2|2),(uid_rx_pkts|2|1),(uid_tx_pkts|2|1),(trusted_time|2|3)
diff --git a/services/java/com/android/server/net/NetworkPolicyManagerService.java b/services/java/com/android/server/net/NetworkPolicyManagerService.java
index 51adebe..a71ccb5 100644
--- a/services/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -1573,6 +1573,9 @@
     private long getTotalBytes(NetworkTemplate template, long start, long end) {
         try {
             return mNetworkStats.getSummaryForNetwork(template, start, end).getTotalBytes();
+        } catch (RuntimeException e) {
+            Slog.w(TAG, "problem reading network stats: " + e);
+            return 0;
         } catch (RemoteException e) {
             // ignored; service lives in system_server
             return 0;
diff --git a/services/java/com/android/server/net/NetworkStatsCollection.java b/services/java/com/android/server/net/NetworkStatsCollection.java
new file mode 100644
index 0000000..70038d9
--- /dev/null
+++ b/services/java/com/android/server/net/NetworkStatsCollection.java
@@ -0,0 +1,510 @@
+/*
+ * Copyright (C) 2012 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.net;
+
+import static android.net.NetworkStats.IFACE_ALL;
+import static android.net.NetworkStats.SET_ALL;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.TrafficStats.UID_REMOVED;
+
+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 com.android.internal.os.AtomicFile;
+import com.android.internal.util.FileRotator;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.Objects;
+import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.ProtocolException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+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.
+ */
+public class NetworkStatsCollection implements FileRotator.Reader {
+    private static final String TAG = "NetworkStatsCollection";
+
+    /** File header magic number: "ANET" */
+    private static final int FILE_MAGIC = 0x414E4554;
+
+    private static final int VERSION_NETWORK_INIT = 1;
+
+    private static final int VERSION_UID_INIT = 1;
+    private static final int VERSION_UID_WITH_IDENT = 2;
+    private static final int VERSION_UID_WITH_TAG = 3;
+    private static final int VERSION_UID_WITH_SET = 4;
+
+    private static final int VERSION_UNIFIED_INIT = 16;
+
+    private HashMap<Key, NetworkStatsHistory> mStats = Maps.newHashMap();
+
+    private long mBucketDuration;
+
+    private long mStartMillis;
+    private long mEndMillis;
+    private long mTotalBytes;
+    private boolean mDirty;
+
+    public NetworkStatsCollection(long bucketDuration) {
+        mBucketDuration = bucketDuration;
+        reset();
+    }
+
+    public void reset() {
+        mStats.clear();
+        mStartMillis = Long.MAX_VALUE;
+        mEndMillis = Long.MIN_VALUE;
+        mTotalBytes = 0;
+        mDirty = false;
+    }
+
+    public long getStartMillis() {
+        return mStartMillis;
+    }
+
+    public long getEndMillis() {
+        return mEndMillis;
+    }
+
+    public long getTotalBytes() {
+        return mTotalBytes;
+    }
+
+    public boolean isDirty() {
+        return mDirty;
+    }
+
+    public void clearDirty() {
+        mDirty = false;
+    }
+
+    public boolean isEmpty() {
+        return mStartMillis == Long.MAX_VALUE && mEndMillis == Long.MIN_VALUE;
+    }
+
+    /**
+     * Combine all {@link NetworkStatsHistory} in this collection which match
+     * the requested parameters.
+     */
+    public NetworkStatsHistory getHistory(
+            NetworkTemplate template, int uid, int set, int tag, int fields) {
+        final NetworkStatsHistory combined = new NetworkStatsHistory(
+                mBucketDuration, estimateBuckets(), fields);
+        for (Map.Entry<Key, NetworkStatsHistory> entry : mStats.entrySet()) {
+            final Key key = entry.getKey();
+            final boolean setMatches = set == SET_ALL || key.set == set;
+            if (key.uid == uid && setMatches && key.tag == tag
+                    && templateMatches(template, key.ident)) {
+                combined.recordEntireHistory(entry.getValue());
+            }
+        }
+        return combined;
+    }
+
+    /**
+     * Summarize all {@link NetworkStatsHistory} in this collection which match
+     * the requested parameters.
+     */
+    public NetworkStats getSummary(NetworkTemplate template, long start, long end) {
+        final long now = System.currentTimeMillis();
+
+        final NetworkStats stats = new NetworkStats(end - start, 24);
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+        NetworkStatsHistory.Entry historyEntry = null;
+
+        for (Map.Entry<Key, NetworkStatsHistory> mapEntry : mStats.entrySet()) {
+            final Key key = mapEntry.getKey();
+            if (templateMatches(template, key.ident)) {
+                final NetworkStatsHistory history = mapEntry.getValue();
+                historyEntry = history.getValues(start, end, now, historyEntry);
+
+                entry.iface = IFACE_ALL;
+                entry.uid = key.uid;
+                entry.set = key.set;
+                entry.tag = key.tag;
+                entry.rxBytes = historyEntry.rxBytes;
+                entry.rxPackets = historyEntry.rxPackets;
+                entry.txBytes = historyEntry.txBytes;
+                entry.txPackets = historyEntry.txPackets;
+                entry.operations = historyEntry.operations;
+
+                if (!entry.isEmpty()) {
+                    stats.combineValues(entry);
+                }
+            }
+        }
+
+        return stats;
+    }
+
+    /**
+     * Record given {@link NetworkStats.Entry} into this collection.
+     */
+    public void recordData(NetworkIdentitySet ident, int uid, int set, int tag, long start,
+            long end, NetworkStats.Entry entry) {
+        noteRecordedHistory(start, end, entry.rxBytes + entry.txBytes);
+        findOrCreateHistory(ident, uid, set, tag).recordData(start, end, entry);
+    }
+
+    /**
+     * Record given {@link NetworkStatsHistory} into this collection.
+     */
+    private void recordHistory(Key key, NetworkStatsHistory history) {
+        if (history.size() == 0) return;
+        noteRecordedHistory(history.getStart(), history.getEnd(), history.getTotalBytes());
+
+        final NetworkStatsHistory existing = mStats.get(key);
+        if (existing != null) {
+            existing.recordEntireHistory(history);
+        } else {
+            mStats.put(key, history);
+        }
+    }
+
+    /**
+     * Record all {@link NetworkStatsHistory} contained in the given collection
+     * into this collection.
+     */
+    public void recordCollection(NetworkStatsCollection another) {
+        for (Map.Entry<Key, NetworkStatsHistory> entry : another.mStats.entrySet()) {
+            recordHistory(entry.getKey(), entry.getValue());
+        }
+    }
+
+    private NetworkStatsHistory findOrCreateHistory(
+            NetworkIdentitySet ident, int uid, int set, int tag) {
+        final Key key = new Key(ident, uid, set, tag);
+        final NetworkStatsHistory existing = mStats.get(key);
+
+        // update when no existing, or when bucket duration changed
+        NetworkStatsHistory updated = null;
+        if (existing == null) {
+            updated = new NetworkStatsHistory(mBucketDuration, 10);
+        } else if (existing.getBucketDuration() != mBucketDuration) {
+            updated = new NetworkStatsHistory(existing, mBucketDuration);
+        }
+
+        if (updated != null) {
+            mStats.put(key, updated);
+            return updated;
+        } else {
+            return existing;
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void read(InputStream in) throws IOException {
+        read(new DataInputStream(in));
+    }
+
+    public void read(DataInputStream in) throws IOException {
+        // verify file magic header intact
+        final int magic = in.readInt();
+        if (magic != FILE_MAGIC) {
+            throw new ProtocolException("unexpected magic: " + magic);
+        }
+
+        final int version = in.readInt();
+        switch (version) {
+            case VERSION_UNIFIED_INIT: {
+                // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory))
+                final int identSize = in.readInt();
+                for (int i = 0; i < identSize; i++) {
+                    final NetworkIdentitySet ident = new NetworkIdentitySet(in);
+
+                    final int size = in.readInt();
+                    for (int j = 0; j < size; j++) {
+                        final int uid = in.readInt();
+                        final int set = in.readInt();
+                        final int tag = in.readInt();
+
+                        final Key key = new Key(ident, uid, set, tag);
+                        final NetworkStatsHistory history = new NetworkStatsHistory(in);
+                        recordHistory(key, history);
+                    }
+                }
+                break;
+            }
+            default: {
+                throw new ProtocolException("unexpected version: " + version);
+            }
+        }
+    }
+
+    public void write(DataOutputStream out) throws IOException {
+        // cluster key lists grouped by ident
+        final HashMap<NetworkIdentitySet, ArrayList<Key>> keysByIdent = Maps.newHashMap();
+        for (Key key : mStats.keySet()) {
+            ArrayList<Key> keys = keysByIdent.get(key.ident);
+            if (keys == null) {
+                keys = Lists.newArrayList();
+                keysByIdent.put(key.ident, keys);
+            }
+            keys.add(key);
+        }
+
+        out.writeInt(FILE_MAGIC);
+        out.writeInt(VERSION_UNIFIED_INIT);
+
+        out.writeInt(keysByIdent.size());
+        for (NetworkIdentitySet ident : keysByIdent.keySet()) {
+            final ArrayList<Key> keys = keysByIdent.get(ident);
+            ident.writeToStream(out);
+
+            out.writeInt(keys.size());
+            for (Key key : keys) {
+                final NetworkStatsHistory history = mStats.get(key);
+                out.writeInt(key.uid);
+                out.writeInt(key.set);
+                out.writeInt(key.tag);
+                history.writeToStream(out);
+            }
+        }
+
+        out.flush();
+    }
+
+    @Deprecated
+    public void readLegacyNetwork(File file) throws IOException {
+        final AtomicFile inputFile = new AtomicFile(file);
+
+        DataInputStream in = null;
+        try {
+            in = new DataInputStream(new BufferedInputStream(inputFile.openRead()));
+
+            // verify file magic header intact
+            final int magic = in.readInt();
+            if (magic != FILE_MAGIC) {
+                throw new ProtocolException("unexpected magic: " + magic);
+            }
+
+            final int version = in.readInt();
+            switch (version) {
+                case VERSION_NETWORK_INIT: {
+                    // network := size *(NetworkIdentitySet NetworkStatsHistory)
+                    final int size = in.readInt();
+                    for (int i = 0; i < size; i++) {
+                        final NetworkIdentitySet ident = new NetworkIdentitySet(in);
+                        final NetworkStatsHistory history = new NetworkStatsHistory(in);
+
+                        final Key key = new Key(ident, UID_ALL, SET_ALL, TAG_NONE);
+                        recordHistory(key, history);
+                    }
+                    break;
+                }
+                default: {
+                    throw new ProtocolException("unexpected version: " + version);
+                }
+            }
+        } catch (FileNotFoundException e) {
+            // missing stats is okay, probably first boot
+        } finally {
+            IoUtils.closeQuietly(in);
+        }
+    }
+
+    @Deprecated
+    public void readLegacyUid(File file, boolean onlyTags) throws IOException {
+        final AtomicFile inputFile = new AtomicFile(file);
+
+        DataInputStream in = null;
+        try {
+            in = new DataInputStream(new BufferedInputStream(inputFile.openRead()));
+
+            // verify file magic header intact
+            final int magic = in.readInt();
+            if (magic != FILE_MAGIC) {
+                throw new ProtocolException("unexpected magic: " + magic);
+            }
+
+            final int version = in.readInt();
+            switch (version) {
+                case VERSION_UID_INIT: {
+                    // uid := size *(UID NetworkStatsHistory)
+
+                    // drop this data version, since we don't have a good
+                    // mapping into NetworkIdentitySet.
+                    break;
+                }
+                case VERSION_UID_WITH_IDENT: {
+                    // uid := size *(NetworkIdentitySet size *(UID NetworkStatsHistory))
+
+                    // drop this data version, since this version only existed
+                    // for a short time.
+                    break;
+                }
+                case VERSION_UID_WITH_TAG:
+                case VERSION_UID_WITH_SET: {
+                    // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory))
+                    final int identSize = in.readInt();
+                    for (int i = 0; i < identSize; i++) {
+                        final NetworkIdentitySet ident = new NetworkIdentitySet(in);
+
+                        final int size = in.readInt();
+                        for (int j = 0; j < size; j++) {
+                            final int uid = in.readInt();
+                            final int set = (version >= VERSION_UID_WITH_SET) ? in.readInt()
+                                    : SET_DEFAULT;
+                            final int tag = in.readInt();
+
+                            final Key key = new Key(ident, uid, set, tag);
+                            final NetworkStatsHistory history = new NetworkStatsHistory(in);
+
+                            if ((tag == TAG_NONE) != onlyTags) {
+                                recordHistory(key, history);
+                            }
+                        }
+                    }
+                    break;
+                }
+                default: {
+                    throw new ProtocolException("unexpected version: " + version);
+                }
+            }
+        } catch (FileNotFoundException e) {
+            // missing stats is okay, probably first boot
+        } finally {
+            IoUtils.closeQuietly(in);
+        }
+    }
+
+    /**
+     * Remove any {@link NetworkStatsHistory} attributed to the requested UID,
+     * moving any {@link NetworkStats#TAG_NONE} series to
+     * {@link TrafficStats#UID_REMOVED}.
+     */
+    public void removeUid(int uid) {
+        final ArrayList<Key> knownKeys = Lists.newArrayList();
+        knownKeys.addAll(mStats.keySet());
+
+        // migrate all UID stats into special "removed" bucket
+        for (Key key : knownKeys) {
+            if (key.uid == uid) {
+                // only migrate combined TAG_NONE history
+                if (key.tag == TAG_NONE) {
+                    final NetworkStatsHistory uidHistory = mStats.get(key);
+                    final NetworkStatsHistory removedHistory = findOrCreateHistory(
+                            key.ident, UID_REMOVED, SET_DEFAULT, TAG_NONE);
+                    removedHistory.recordEntireHistory(uidHistory);
+                }
+                mStats.remove(key);
+                mDirty = true;
+            }
+        }
+    }
+
+    private void noteRecordedHistory(long startMillis, long endMillis, long totalBytes) {
+        if (startMillis < mStartMillis) mStartMillis = startMillis;
+        if (endMillis > mEndMillis) mEndMillis = endMillis;
+        mTotalBytes += totalBytes;
+        mDirty = true;
+    }
+
+    private int estimateBuckets() {
+        return (int) (Math.min(mEndMillis - mStartMillis, DateUtils.WEEK_IN_MILLIS * 5)
+                / mBucketDuration);
+    }
+
+    public void dump(IndentingPrintWriter pw) {
+        final ArrayList<Key> keys = Lists.newArrayList();
+        keys.addAll(mStats.keySet());
+        Collections.sort(keys);
+
+        for (Key key : keys) {
+            pw.print("ident="); pw.print(key.ident.toString());
+            pw.print(" uid="); pw.print(key.uid);
+            pw.print(" set="); pw.print(NetworkStats.setToString(key.set));
+            pw.print(" tag="); pw.println(NetworkStats.tagToString(key.tag));
+
+            final NetworkStatsHistory history = mStats.get(key);
+            pw.increaseIndent();
+            history.dump(pw, true);
+            pw.decreaseIndent();
+        }
+    }
+
+    /**
+     * Test if given {@link NetworkTemplate} matches any {@link NetworkIdentity}
+     * in the given {@link NetworkIdentitySet}.
+     */
+    private static boolean templateMatches(NetworkTemplate template, NetworkIdentitySet identSet) {
+        for (NetworkIdentity ident : identSet) {
+            if (template.matches(ident)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static class Key implements Comparable<Key> {
+        public final NetworkIdentitySet ident;
+        public final int uid;
+        public final int set;
+        public final int tag;
+
+        private final int hashCode;
+
+        public Key(NetworkIdentitySet ident, int uid, int set, int tag) {
+            this.ident = ident;
+            this.uid = uid;
+            this.set = set;
+            this.tag = tag;
+            hashCode = Objects.hashCode(ident, uid, set, tag);
+        }
+
+        @Override
+        public int hashCode() {
+            return hashCode;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof Key) {
+                final Key key = (Key) obj;
+                return uid == key.uid && set == key.set && tag == key.tag
+                        && Objects.equal(ident, key.ident);
+            }
+            return false;
+        }
+
+        /** {@inheritDoc} */
+        public int compareTo(Key another) {
+            return Integer.compare(uid, another.uid);
+        }
+    }
+}
diff --git a/services/java/com/android/server/net/NetworkStatsRecorder.java b/services/java/com/android/server/net/NetworkStatsRecorder.java
new file mode 100644
index 0000000..e7ba358
--- /dev/null
+++ b/services/java/com/android/server/net/NetworkStatsRecorder.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2012 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.net;
+
+import static android.net.NetworkStats.TAG_NONE;
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.net.NetworkStats;
+import android.net.NetworkStats.NonMonotonicObserver;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.net.TrafficStats;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.util.FileRotator;
+import com.android.internal.util.IndentingPrintWriter;
+import com.google.android.collect.Sets;
+
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.ref.WeakReference;
+import java.util.HashSet;
+import java.util.Map;
+
+/**
+ * Logic to record deltas between periodic {@link NetworkStats} snapshots into
+ * {@link NetworkStatsHistory} that belong to {@link NetworkStatsCollection}.
+ * Keeps pending changes in memory until they pass a specific threshold, in
+ * bytes. Uses {@link FileRotator} for persistence logic.
+ * <p>
+ * Not inherently thread safe.
+ */
+public class NetworkStatsRecorder {
+    private static final String TAG = "NetworkStatsRecorder";
+    private static final boolean LOGD = true;
+
+    private final FileRotator mRotator;
+    private final NonMonotonicObserver<String> mObserver;
+    private final String mCookie;
+
+    private final long mBucketDuration;
+    private final long mPersistThresholdBytes;
+    private final boolean mOnlyTags;
+
+    private NetworkStats mLastSnapshot;
+
+    private final NetworkStatsCollection mPending;
+    private final NetworkStatsCollection mSinceBoot;
+
+    private final CombiningRewriter mPendingRewriter;
+
+    private WeakReference<NetworkStatsCollection> mComplete;
+
+    public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
+            String cookie, long bucketDuration, long persistThresholdBytes, boolean onlyTags) {
+        mRotator = checkNotNull(rotator, "missing FileRotator");
+        mObserver = checkNotNull(observer, "missing NonMonotonicObserver");
+        mCookie = cookie;
+
+        mBucketDuration = bucketDuration;
+        mPersistThresholdBytes = persistThresholdBytes;
+        mOnlyTags = onlyTags;
+
+        mPending = new NetworkStatsCollection(bucketDuration);
+        mSinceBoot = new NetworkStatsCollection(bucketDuration);
+
+        mPendingRewriter = new CombiningRewriter(mPending);
+    }
+
+    public void resetLocked() {
+        mLastSnapshot = null;
+        mPending.reset();
+        mSinceBoot.reset();
+        mComplete.clear();
+    }
+
+    public NetworkStats.Entry getTotalSinceBootLocked(NetworkTemplate template) {
+        return mSinceBoot.getSummary(template, Long.MIN_VALUE, Long.MAX_VALUE).getTotal(null);
+    }
+
+    /**
+     * Load complete history represented by {@link FileRotator}. Caches
+     * internally as a {@link WeakReference}, and updated with future
+     * {@link #recordSnapshotLocked(NetworkStats, Map, long)} snapshots as long
+     * 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);
+            }
+        }
+        return complete;
+    }
+
+    /**
+     * Record any delta that occurred since last {@link NetworkStats} snapshot,
+     * using the given {@link Map} to identify network interfaces. First
+     * snapshot is considered bootstrap, and is not counted as delta.
+     */
+    public void recordSnapshotLocked(NetworkStats snapshot,
+            Map<String, NetworkIdentitySet> ifaceIdent, long currentTimeMillis) {
+        final HashSet<String> unknownIfaces = Sets.newHashSet();
+
+        // assume first snapshot is bootstrap and don't record
+        if (mLastSnapshot == null) {
+            mLastSnapshot = snapshot;
+            return;
+        }
+
+        final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
+
+        final NetworkStats delta = NetworkStats.subtract(
+                snapshot, mLastSnapshot, mObserver, mCookie);
+        final long end = currentTimeMillis;
+        final long start = end - delta.getElapsedRealtime();
+
+        NetworkStats.Entry entry = null;
+        for (int i = 0; i < delta.size(); i++) {
+            entry = delta.getValues(i, entry);
+            final NetworkIdentitySet ident = ifaceIdent.get(entry.iface);
+            if (ident == null) {
+                unknownIfaces.add(entry.iface);
+                continue;
+            }
+
+            // skip when no delta occured
+            if (entry.isEmpty()) continue;
+
+            // only record tag data when requested
+            if ((entry.tag == TAG_NONE) != mOnlyTags) {
+                mPending.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
+
+                // also record against boot stats when present
+                if (mSinceBoot != null) {
+                    mSinceBoot.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
+                }
+
+                // also record against complete dataset when present
+                if (complete != null) {
+                    complete.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
+                }
+            }
+        }
+
+        mLastSnapshot = snapshot;
+
+        if (LOGD && unknownIfaces.size() > 0) {
+            Slog.w(TAG, "unknown interfaces " + unknownIfaces + ", ignoring those stats");
+        }
+    }
+
+    /**
+     * Consider persisting any pending deltas, if they are beyond
+     * {@link #mPersistThresholdBytes}.
+     */
+    public void maybePersistLocked(long currentTimeMillis) {
+        final long pendingBytes = mPending.getTotalBytes();
+        if (pendingBytes >= mPersistThresholdBytes) {
+            forcePersistLocked(currentTimeMillis);
+        } else {
+            mRotator.maybeRotate(currentTimeMillis);
+        }
+    }
+
+    /**
+     * Force persisting any pending deltas.
+     */
+    public void forcePersistLocked(long currentTimeMillis) {
+        if (mPending.isDirty()) {
+            if (LOGD) Slog.d(TAG, "forcePersistLocked() writing for " + mCookie);
+            try {
+                mRotator.rewriteActive(mPendingRewriter, currentTimeMillis);
+                mRotator.maybeRotate(currentTimeMillis);
+                mPending.reset();
+            } catch (IOException e) {
+                Log.wtf(TAG, "problem persisting pending stats", e);
+            }
+        }
+    }
+
+    /**
+     * Remove the given UID from all {@link FileRotator} history, migrating it
+     * to {@link TrafficStats#UID_REMOVED}.
+     */
+    public void removeUidLocked(int uid) {
+        try {
+            // process all existing data to migrate uid
+            mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uid));
+        } catch (IOException e) {
+            Log.wtf(TAG, "problem removing UID " + uid, e);
+        }
+
+        // clear UID from current stats snapshot
+        if (mLastSnapshot != null) {
+            mLastSnapshot = mLastSnapshot.withoutUid(uid);
+        }
+    }
+
+    /**
+     * Rewriter that will combine current {@link NetworkStatsCollection} values
+     * with anything read from disk, and write combined set to disk. Clears the
+     * original {@link NetworkStatsCollection} when finished writing.
+     */
+    private static class CombiningRewriter implements FileRotator.Rewriter {
+        private final NetworkStatsCollection mCollection;
+
+        public CombiningRewriter(NetworkStatsCollection collection) {
+            mCollection = checkNotNull(collection, "missing NetworkStatsCollection");
+        }
+
+        /** {@inheritDoc} */
+        public void reset() {
+            // ignored
+        }
+
+        /** {@inheritDoc} */
+        public void read(InputStream in) throws IOException {
+            mCollection.read(in);
+        }
+
+        /** {@inheritDoc} */
+        public boolean shouldWrite() {
+            return true;
+        }
+
+        /** {@inheritDoc} */
+        public void write(OutputStream out) throws IOException {
+            mCollection.write(new DataOutputStream(out));
+            mCollection.reset();
+        }
+    }
+
+    /**
+     * Rewriter that will remove any {@link NetworkStatsHistory} attributed to
+     * the requested UID, only writing data back when modified.
+     */
+    public static class RemoveUidRewriter implements FileRotator.Rewriter {
+        private final NetworkStatsCollection mTemp;
+        private final int mUid;
+
+        public RemoveUidRewriter(long bucketDuration, int uid) {
+            mTemp = new NetworkStatsCollection(bucketDuration);
+            mUid = uid;
+        }
+
+        /** {@inheritDoc} */
+        public void reset() {
+            mTemp.reset();
+        }
+
+        /** {@inheritDoc} */
+        public void read(InputStream in) throws IOException {
+            mTemp.read(in);
+            mTemp.clearDirty();
+            mTemp.removeUid(mUid);
+        }
+
+        /** {@inheritDoc} */
+        public boolean shouldWrite() {
+            return mTemp.isDirty();
+        }
+
+        /** {@inheritDoc} */
+        public void write(OutputStream out) throws IOException {
+            mTemp.write(new DataOutputStream(out));
+        }
+    }
+
+    public void importLegacyNetworkLocked(File file) throws IOException {
+        // legacy file still exists; start empty to avoid double importing
+        mRotator.deleteAll();
+
+        final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
+        collection.readLegacyNetwork(file);
+
+        final long startMillis = collection.getStartMillis();
+        final long endMillis = collection.getEndMillis();
+
+        if (!collection.isEmpty()) {
+            // process legacy data, creating active file at starting time, then
+            // using end time to possibly trigger rotation.
+            mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
+            mRotator.maybeRotate(endMillis);
+        }
+    }
+
+    public void importLegacyUidLocked(File file) throws IOException {
+        // legacy file still exists; start empty to avoid double importing
+        mRotator.deleteAll();
+
+        final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
+        collection.readLegacyUid(file, mOnlyTags);
+
+        final long startMillis = collection.getStartMillis();
+        final long endMillis = collection.getEndMillis();
+
+        if (!collection.isEmpty()) {
+            // process legacy data, creating active file at starting time, then
+            // using end time to possibly trigger rotation.
+            mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
+            mRotator.maybeRotate(endMillis);
+        }
+    }
+
+    public void dumpLocked(IndentingPrintWriter pw, boolean fullHistory) {
+        pw.print("Pending bytes: "); pw.println(mPending.getTotalBytes());
+        if (fullHistory) {
+            pw.println("Complete history:");
+            getOrLoadCompleteLocked().dump(pw);
+        } else {
+            pw.println("History since boot:");
+            mSinceBoot.dump(pw);
+        }
+    }
+}
diff --git a/services/java/com/android/server/net/NetworkStatsService.java b/services/java/com/android/server/net/NetworkStatsService.java
index eeb7fec..c9b79e8 100644
--- a/services/java/com/android/server/net/NetworkStatsService.java
+++ b/services/java/com/android/server/net/NetworkStatsService.java
@@ -34,14 +34,18 @@
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.NetworkTemplate.buildTemplateMobileAll;
 import static android.net.NetworkTemplate.buildTemplateWifi;
-import static android.net.TrafficStats.UID_REMOVED;
-import static android.provider.Settings.Secure.NETSTATS_NETWORK_BUCKET_DURATION;
-import static android.provider.Settings.Secure.NETSTATS_NETWORK_MAX_HISTORY;
-import static android.provider.Settings.Secure.NETSTATS_PERSIST_THRESHOLD;
+import static android.provider.Settings.Secure.NETSTATS_DEV_BUCKET_DURATION;
+import static android.provider.Settings.Secure.NETSTATS_DEV_DELETE_AGE;
+import static android.provider.Settings.Secure.NETSTATS_DEV_PERSIST_BYTES;
+import static android.provider.Settings.Secure.NETSTATS_DEV_ROTATE_AGE;
+import static android.provider.Settings.Secure.NETSTATS_GLOBAL_ALERT_BYTES;
 import static android.provider.Settings.Secure.NETSTATS_POLL_INTERVAL;
-import static android.provider.Settings.Secure.NETSTATS_TAG_MAX_HISTORY;
+import static android.provider.Settings.Secure.NETSTATS_SAMPLE_ENABLED;
+import static android.provider.Settings.Secure.NETSTATS_TIME_CACHE_MAX_AGE;
 import static android.provider.Settings.Secure.NETSTATS_UID_BUCKET_DURATION;
-import static android.provider.Settings.Secure.NETSTATS_UID_MAX_HISTORY;
+import static android.provider.Settings.Secure.NETSTATS_UID_DELETE_AGE;
+import static android.provider.Settings.Secure.NETSTATS_UID_PERSIST_BYTES;
+import static android.provider.Settings.Secure.NETSTATS_UID_ROTATE_AGE;
 import static android.telephony.PhoneStateListener.LISTEN_DATA_CONNECTION_STATE;
 import static android.telephony.PhoneStateListener.LISTEN_NONE;
 import static android.text.format.DateUtils.DAY_IN_MILLIS;
@@ -61,12 +65,10 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
 import android.net.IConnectivityManager;
 import android.net.INetworkManagementEventObserver;
 import android.net.INetworkStatsService;
+import android.net.LinkProperties;
 import android.net.NetworkIdentity;
 import android.net.NetworkInfo;
 import android.net.NetworkState;
@@ -74,6 +76,7 @@
 import android.net.NetworkStats.NonMonotonicObserver;
 import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
+import android.net.TrafficStats;
 import android.os.Binder;
 import android.os.DropBoxManager;
 import android.os.Environment;
@@ -94,32 +97,18 @@
 import android.util.SparseIntArray;
 import android.util.TrustedTime;
 
-import com.android.internal.os.AtomicFile;
-import com.android.internal.util.Objects;
+import com.android.internal.util.FileRotator;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.EventLogTags;
 import com.android.server.connectivity.Tethering;
-import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
-import com.google.android.collect.Sets;
 
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
 import java.io.File;
 import java.io.FileDescriptor;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
-import java.net.ProtocolException;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Random;
-
-import libcore.io.IoUtils;
 
 /**
  * Collect and persist detailed network statistics, and provide this data to
@@ -127,16 +116,8 @@
  */
 public class NetworkStatsService extends INetworkStatsService.Stub {
     private static final String TAG = "NetworkStats";
-    private static final boolean LOGD = false;
-    private static final boolean LOGV = false;
-
-    /** File header magic number: "ANET" */
-    private static final int FILE_MAGIC = 0x414E4554;
-    private static final int VERSION_NETWORK_INIT = 1;
-    private static final int VERSION_UID_INIT = 1;
-    private static final int VERSION_UID_WITH_IDENT = 2;
-    private static final int VERSION_UID_WITH_TAG = 3;
-    private static final int VERSION_UID_WITH_SET = 4;
+    private static final boolean LOGD = true;
+    private static final boolean LOGV = true;
 
     private static final int MSG_PERFORM_POLL = 1;
     private static final int MSG_UPDATE_IFACES = 2;
@@ -147,9 +128,6 @@
     private static final int FLAG_PERSIST_ALL = FLAG_PERSIST_NETWORK | FLAG_PERSIST_UID;
     private static final int FLAG_PERSIST_FORCE = 0x100;
 
-    /** Sample recent usage after each poll event. */
-    private static final boolean ENABLE_SAMPLE_AFTER_POLL = true;
-
     private static final String TAG_NETSTATS_ERROR = "netstats_error";
 
     private final Context mContext;
@@ -159,10 +137,12 @@
     private final TelephonyManager mTeleManager;
     private final NetworkStatsSettings mSettings;
 
+    private final File mSystemDir;
+    private final File mBaseDir;
+
     private final PowerManager.WakeLock mWakeLock;
 
     private IConnectivityManager mConnManager;
-    private DropBoxManager mDropBox;
 
     // @VisibleForTesting
     public static final String ACTION_NETWORK_STATS_POLL =
@@ -172,71 +152,76 @@
 
     private PendingIntent mPollIntent;
 
-    // TODO: trim empty history objects entirely
-
     private static final long KB_IN_BYTES = 1024;
     private static final long MB_IN_BYTES = 1024 * KB_IN_BYTES;
     private static final long GB_IN_BYTES = 1024 * MB_IN_BYTES;
 
+    private static final String PREFIX_DEV = "dev";
+    private static final String PREFIX_UID = "uid";
+    private static final String PREFIX_UID_TAG = "uid_tag";
+
     /**
      * Settings that can be changed externally.
      */
     public interface NetworkStatsSettings {
         public long getPollInterval();
-        public long getPersistThreshold();
-        public long getNetworkBucketDuration();
-        public long getNetworkMaxHistory();
-        public long getUidBucketDuration();
-        public long getUidMaxHistory();
-        public long getTagMaxHistory();
         public long getTimeCacheMaxAge();
+        public long getGlobalAlertBytes();
+        public boolean getSampleEnabled();
+
+        public static class Config {
+            public final long bucketDuration;
+            public final long persistBytes;
+            public final long rotateAgeMillis;
+            public final long deleteAgeMillis;
+
+            public Config(long bucketDuration, long persistBytes, long rotateAgeMillis,
+                    long deleteAgeMillis) {
+                this.bucketDuration = bucketDuration;
+                this.persistBytes = persistBytes;
+                this.rotateAgeMillis = rotateAgeMillis;
+                this.deleteAgeMillis = deleteAgeMillis;
+            }
+        }
+
+        public Config getDevConfig();
+        public Config getUidConfig();
+        public Config getUidTagConfig();
     }
 
     private final Object mStatsLock = new Object();
 
     /** Set of currently active ifaces. */
     private HashMap<String, NetworkIdentitySet> mActiveIfaces = Maps.newHashMap();
-    /** Set of historical {@code dev} stats for known networks. */
-    private HashMap<NetworkIdentitySet, NetworkStatsHistory> mNetworkDevStats = Maps.newHashMap();
-    /** Set of historical {@code xtables} stats for known networks. */
-    private HashMap<NetworkIdentitySet, NetworkStatsHistory> mNetworkXtStats = Maps.newHashMap();
-    /** Set of historical {@code xtables} stats for known UIDs. */
-    private HashMap<UidStatsKey, NetworkStatsHistory> mUidStats = Maps.newHashMap();
+    /** Current default active iface. */
+    private String mActiveIface;
 
-    /** Flag if {@link #mNetworkDevStats} have been loaded from disk. */
-    private boolean mNetworkStatsLoaded = false;
-    /** Flag if {@link #mUidStats} have been loaded from disk. */
-    private boolean mUidStatsLoaded = false;
+    private final DropBoxNonMonotonicObserver mNonMonotonicObserver =
+            new DropBoxNonMonotonicObserver();
 
-    private NetworkStats mLastPollNetworkDevSnapshot;
-    private NetworkStats mLastPollNetworkXtSnapshot;
-    private NetworkStats mLastPollUidSnapshot;
-    private NetworkStats mLastPollOperationsSnapshot;
+    private NetworkStatsRecorder mDevRecorder;
+    private NetworkStatsRecorder mUidRecorder;
+    private NetworkStatsRecorder mUidTagRecorder;
 
-    private NetworkStats mLastPersistNetworkDevSnapshot;
-    private NetworkStats mLastPersistNetworkXtSnapshot;
-    private NetworkStats mLastPersistUidSnapshot;
+    /** Cached {@link #mDevRecorder} stats. */
+    private NetworkStatsCollection mDevStatsCached;
 
     /** Current counter sets for each UID. */
     private SparseIntArray mActiveUidCounterSet = new SparseIntArray();
 
     /** Data layer operation counters for splicing into other structures. */
-    private NetworkStats mOperations = new NetworkStats(0L, 10);
+    private NetworkStats mUidOperations = new NetworkStats(0L, 10);
 
     private final HandlerThread mHandlerThread;
     private final Handler mHandler;
 
-    private final AtomicFile mNetworkDevFile;
-    private final AtomicFile mNetworkXtFile;
-    private final AtomicFile mUidFile;
-
     public NetworkStatsService(
             Context context, INetworkManagementService networkManager, IAlarmManager alarmManager) {
         this(context, networkManager, alarmManager, NtpTrustedTime.getInstance(context),
-                getSystemDir(), new DefaultNetworkStatsSettings(context));
+                getDefaultSystemDir(), new DefaultNetworkStatsSettings(context));
     }
 
-    private static File getSystemDir() {
+    private static File getDefaultSystemDir() {
         return new File(Environment.getDataDirectory(), "system");
     }
 
@@ -258,9 +243,9 @@
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper(), mHandlerCallback);
 
-        mNetworkDevFile = new AtomicFile(new File(systemDir, "netstats.bin"));
-        mNetworkXtFile = new AtomicFile(new File(systemDir, "netstats_xt.bin"));
-        mUidFile = new AtomicFile(new File(systemDir, "netstats_uid.bin"));
+        mSystemDir = checkNotNull(systemDir);
+        mBaseDir = new File(systemDir, "netstats");
+        mBaseDir.mkdirs();
     }
 
     public void bindConnectivityManager(IConnectivityManager connManager) {
@@ -273,17 +258,22 @@
             return;
         }
 
-        synchronized (mStatsLock) {
-            // read historical network stats from disk, since policy service
-            // might need them right away. we delay loading detailed UID stats
-            // until actually needed.
-            readNetworkDevStatsLocked();
-            readNetworkXtStatsLocked();
-            mNetworkStatsLoaded = true;
-        }
+        // create data recorders along with historical rotators
+        mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false);
+        mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false);
+        mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true);
 
-        // bootstrap initial stats to prevent double-counting later
-        bootstrapStats();
+        synchronized (mStatsLock) {
+            // upgrade any legacy stats, migrating them to rotated files
+            maybeUpgradeLegacyStatsLocked();
+
+            // read historical network stats from disk, since policy service
+            // might need them right away.
+            mDevStatsCached = mDevRecorder.getOrLoadCompleteLocked();
+
+            // bootstrap initial stats to prevent double-counting later
+            bootstrapStatsLocked();
+        }
 
         // watch for network interfaces to be claimed
         final IntentFilter connFilter = new IntentFilter(CONNECTIVITY_ACTION_IMMEDIATE);
@@ -317,8 +307,14 @@
 
         registerPollAlarmLocked();
         registerGlobalAlert();
+    }
 
-        mDropBox = (DropBoxManager) mContext.getSystemService(Context.DROPBOX_SERVICE);
+    private NetworkStatsRecorder buildRecorder(
+            String prefix, NetworkStatsSettings.Config config, boolean includeTags) {
+        return new NetworkStatsRecorder(
+                new FileRotator(mBaseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
+                mNonMonotonicObserver, prefix, config.bucketDuration, config.persistBytes,
+                includeTags);
     }
 
     private void shutdownLocked() {
@@ -330,18 +326,44 @@
 
         mTeleManager.listen(mPhoneListener, LISTEN_NONE);
 
-        if (mNetworkStatsLoaded) {
-            writeNetworkDevStatsLocked();
-            writeNetworkXtStatsLocked();
+        final long currentTime = mTime.hasCache() ? mTime.currentTimeMillis()
+                : System.currentTimeMillis();
+
+        // persist any pending stats
+        mDevRecorder.forcePersistLocked(currentTime);
+        mUidRecorder.forcePersistLocked(currentTime);
+        mUidTagRecorder.forcePersistLocked(currentTime);
+
+        mDevRecorder = null;
+        mUidRecorder = null;
+        mUidTagRecorder = null;
+
+        mDevStatsCached = null;
+    }
+
+    private void maybeUpgradeLegacyStatsLocked() {
+        File file;
+        try {
+            file = new File(mSystemDir, "netstats.bin");
+            if (file.exists()) {
+                mDevRecorder.importLegacyNetworkLocked(file);
+                file.delete();
+            }
+
+            file = new File(mSystemDir, "netstats_xt.bin");
+            if (file.exists()) {
+                file.delete();
+            }
+
+            file = new File(mSystemDir, "netstats_uid.bin");
+            if (file.exists()) {
+                mUidRecorder.importLegacyUidLocked(file);
+                mUidTagRecorder.importLegacyUidLocked(file);
+                file.delete();
+            }
+        } catch (IOException e) {
+            Log.wtf(TAG, "problem during legacy upgrade", e);
         }
-        if (mUidStatsLoaded) {
-            writeUidStatsLocked();
-        }
-        mNetworkDevStats.clear();
-        mNetworkXtStats.clear();
-        mUidStats.clear();
-        mNetworkStatsLoaded = false;
-        mUidStatsLoaded = false;
     }
 
     /**
@@ -372,7 +394,7 @@
      */
     private void registerGlobalAlert() {
         try {
-            final long alertBytes = mSettings.getPersistThreshold();
+            final long alertBytes = mSettings.getGlobalAlertBytes();
             mNetworkManager.setGlobalAlert(alertBytes);
         } catch (IllegalStateException e) {
             Slog.w(TAG, "problem registering for global alert: " + e);
@@ -383,161 +405,39 @@
 
     @Override
     public NetworkStatsHistory getHistoryForNetwork(NetworkTemplate template, int fields) {
-        mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG);
-        return getHistoryForNetworkDev(template, fields);
+        return mDevStatsCached.getHistory(template, UID_ALL, SET_ALL, TAG_NONE, fields);
     }
 
-    private NetworkStatsHistory getHistoryForNetworkDev(NetworkTemplate template, int fields) {
-        return getHistoryForNetwork(template, fields, mNetworkDevStats);
-    }
-
-    private NetworkStatsHistory getHistoryForNetworkXt(NetworkTemplate template, int fields) {
-        return getHistoryForNetwork(template, fields, mNetworkXtStats);
-    }
-
-    private NetworkStatsHistory getHistoryForNetwork(NetworkTemplate template, int fields,
-            HashMap<NetworkIdentitySet, NetworkStatsHistory> source) {
-        synchronized (mStatsLock) {
-            // combine all interfaces that match template
-            final NetworkStatsHistory combined = new NetworkStatsHistory(
-                    mSettings.getNetworkBucketDuration(), estimateNetworkBuckets(), fields);
-            for (NetworkIdentitySet ident : source.keySet()) {
-                if (templateMatches(template, ident)) {
-                    final NetworkStatsHistory history = source.get(ident);
-                    if (history != null) {
-                        combined.recordEntireHistory(history);
-                    }
-                }
-            }
-            return combined;
-        }
+    @Override
+    public NetworkStats getSummaryForNetwork(NetworkTemplate template, long start, long end) {
+        return mDevStatsCached.getSummary(template, start, end);
     }
 
     @Override
     public NetworkStatsHistory getHistoryForUid(
             NetworkTemplate template, int uid, int set, int tag, int fields) {
-        mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG);
-
-        synchronized (mStatsLock) {
-            ensureUidStatsLoadedLocked();
-
-            // combine all interfaces that match template
-            final NetworkStatsHistory combined = new NetworkStatsHistory(
-                    mSettings.getUidBucketDuration(), estimateUidBuckets(), fields);
-            for (UidStatsKey key : mUidStats.keySet()) {
-                final boolean setMatches = set == SET_ALL || key.set == set;
-                if (templateMatches(template, key.ident) && key.uid == uid && setMatches
-                        && key.tag == tag) {
-                    final NetworkStatsHistory history = mUidStats.get(key);
-                    combined.recordEntireHistory(history);
-                }
-            }
-
-            return combined;
+        // TODO: transition to stats sessions to avoid WeakReferences
+        if (tag == TAG_NONE) {
+            return mUidRecorder.getOrLoadCompleteLocked().getHistory(
+                    template, uid, set, tag, fields);
+        } else {
+            return mUidTagRecorder.getOrLoadCompleteLocked().getHistory(
+                    template, uid, set, tag, fields);
         }
     }
 
     @Override
-    public NetworkStats getSummaryForNetwork(NetworkTemplate template, long start, long end) {
-        mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG);
-        return getSummaryForNetworkDev(template, start, end);
-    }
-
-    private NetworkStats getSummaryForNetworkDev(NetworkTemplate template, long start, long end) {
-        return getSummaryForNetwork(template, start, end, mNetworkDevStats);
-    }
-
-    private NetworkStats getSummaryForNetworkXt(NetworkTemplate template, long start, long end) {
-        return getSummaryForNetwork(template, start, end, mNetworkXtStats);
-    }
-
-    private NetworkStats getSummaryForNetwork(NetworkTemplate template, long start, long end,
-            HashMap<NetworkIdentitySet, NetworkStatsHistory> source) {
-        synchronized (mStatsLock) {
-            // use system clock to be externally consistent
-            final long now = System.currentTimeMillis();
-
-            final NetworkStats stats = new NetworkStats(end - start, 1);
-            final NetworkStats.Entry entry = new NetworkStats.Entry();
-            NetworkStatsHistory.Entry historyEntry = null;
-
-            // combine total from all interfaces that match template
-            for (NetworkIdentitySet ident : source.keySet()) {
-                if (templateMatches(template, ident)) {
-                    final NetworkStatsHistory history = source.get(ident);
-                    historyEntry = history.getValues(start, end, now, historyEntry);
-
-                    entry.iface = IFACE_ALL;
-                    entry.uid = UID_ALL;
-                    entry.tag = TAG_NONE;
-                    entry.rxBytes = historyEntry.rxBytes;
-                    entry.rxPackets = historyEntry.rxPackets;
-                    entry.txBytes = historyEntry.txBytes;
-                    entry.txPackets = historyEntry.txPackets;
-
-                    stats.combineValues(entry);
-                }
-            }
-
-            return stats;
-        }
-    }
-
-    private long getHistoryStartLocked(
-            NetworkTemplate template, HashMap<NetworkIdentitySet, NetworkStatsHistory> source) {
-        long start = Long.MAX_VALUE;
-        for (NetworkIdentitySet ident : source.keySet()) {
-            if (templateMatches(template, ident)) {
-                final NetworkStatsHistory history = source.get(ident);
-                start = Math.min(start, history.getStart());
-            }
-        }
-        return start;
-    }
-
-    @Override
     public NetworkStats getSummaryForAllUid(
             NetworkTemplate template, long start, long end, boolean includeTags) {
-        mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG);
-
-        synchronized (mStatsLock) {
-            ensureUidStatsLoadedLocked();
-
-            // use system clock to be externally consistent
-            final long now = System.currentTimeMillis();
-
-            final NetworkStats stats = new NetworkStats(end - start, 24);
-            final NetworkStats.Entry entry = new NetworkStats.Entry();
-            NetworkStatsHistory.Entry historyEntry = null;
-
-            for (UidStatsKey key : mUidStats.keySet()) {
-                if (templateMatches(template, key.ident)) {
-                    // always include summary under TAG_NONE, and include
-                    // other tags when requested.
-                    if (key.tag == TAG_NONE || includeTags) {
-                        final NetworkStatsHistory history = mUidStats.get(key);
-                        historyEntry = history.getValues(start, end, now, historyEntry);
-
-                        entry.iface = IFACE_ALL;
-                        entry.uid = key.uid;
-                        entry.set = key.set;
-                        entry.tag = key.tag;
-                        entry.rxBytes = historyEntry.rxBytes;
-                        entry.rxPackets = historyEntry.rxPackets;
-                        entry.txBytes = historyEntry.txBytes;
-                        entry.txPackets = historyEntry.txPackets;
-                        entry.operations = historyEntry.operations;
-
-                        if (entry.rxBytes > 0 || entry.rxPackets > 0 || entry.txBytes > 0
-                                || entry.txPackets > 0 || entry.operations > 0) {
-                            stats.combineValues(entry);
-                        }
-                    }
-                }
-            }
-
-            return stats;
+        // TODO: transition to stats sessions to avoid WeakReferences
+        final NetworkStats stats = mUidRecorder.getOrLoadCompleteLocked().getSummary(
+                template, start, end);
+        if (includeTags) {
+            final NetworkStats tagStats = mUidTagRecorder.getOrLoadCompleteLocked().getSummary(
+                    template, start, end);
+            stats.combineAllValues(tagStats);
         }
+        return stats;
     }
 
     @Override
@@ -567,7 +467,7 @@
         }
 
         // splice in operation counts
-        dataLayer.spliceOperationsFrom(mOperations);
+        dataLayer.spliceOperationsFrom(mUidOperations);
         return dataLayer;
     }
 
@@ -586,8 +486,10 @@
 
         synchronized (mStatsLock) {
             final int set = mActiveUidCounterSet.get(uid, SET_DEFAULT);
-            mOperations.combineValues(IFACE_ALL, uid, set, tag, 0L, 0L, 0L, 0L, operationCount);
-            mOperations.combineValues(IFACE_ALL, uid, set, TAG_NONE, 0L, 0L, 0L, 0L, operationCount);
+            mUidOperations.combineValues(
+                    mActiveIface, uid, set, tag, 0L, 0L, 0L, 0L, operationCount);
+            mUidOperations.combineValues(
+                    mActiveIface, uid, set, TAG_NONE, 0L, 0L, 0L, 0L, operationCount);
         }
     }
 
@@ -755,13 +657,17 @@
         performPollLocked(FLAG_PERSIST_NETWORK);
 
         final NetworkState[] states;
+        final LinkProperties activeLink;
         try {
             states = mConnManager.getAllNetworkState();
+            activeLink = mConnManager.getActiveLinkProperties();
         } catch (RemoteException e) {
             // ignored; service lives in system_server
             return;
         }
 
+        mActiveIface = activeLink != null ? activeLink.getInterfaceName() : null;
+
         // rebuild active interfaces based on connected networks
         mActiveIfaces.clear();
 
@@ -785,12 +691,20 @@
      * Bootstrap initial stats snapshot, usually during {@link #systemReady()}
      * so we have baseline values without double-counting.
      */
-    private void bootstrapStats() {
+    private void bootstrapStatsLocked() {
+        final long currentTime = mTime.hasCache() ? mTime.currentTimeMillis()
+                : System.currentTimeMillis();
+
         try {
-            mLastPollUidSnapshot = mNetworkManager.getNetworkStatsUidDetail(UID_ALL);
-            mLastPollNetworkDevSnapshot = mNetworkManager.getNetworkStatsSummary();
-            mLastPollNetworkXtSnapshot = computeNetworkXtSnapshotFromUid(mLastPollUidSnapshot);
-            mLastPollOperationsSnapshot = new NetworkStats(0L, 0);
+            // snapshot and record current counters; read UID stats first to
+            // avoid overcounting dev stats.
+            final NetworkStats uidSnapshot = getNetworkStatsUidDetail();
+            final NetworkStats devSnapshot = getNetworkStatsSummary();
+
+            mDevRecorder.recordSnapshotLocked(devSnapshot, mActiveIfaces, currentTime);
+            mUidRecorder.recordSnapshotLocked(uidSnapshot, mActiveIfaces, currentTime);
+            mUidTagRecorder.recordSnapshotLocked(uidSnapshot, mActiveIfaces, currentTime);
+
         } catch (IllegalStateException e) {
             Slog.w(TAG, "problem reading network stats: " + e);
         } catch (RemoteException e) {
@@ -830,27 +744,16 @@
         // TODO: consider marking "untrusted" times in historical stats
         final long currentTime = mTime.hasCache() ? mTime.currentTimeMillis()
                 : System.currentTimeMillis();
-        final long threshold = mSettings.getPersistThreshold();
 
-        final NetworkStats uidSnapshot;
-        final NetworkStats networkXtSnapshot;
-        final NetworkStats networkDevSnapshot;
         try {
-            // collect any tethering stats
-            final NetworkStats tetherSnapshot = getNetworkStatsTethering();
+            // snapshot and record current counters; read UID stats first to
+            // avoid overcounting dev stats.
+            final NetworkStats uidSnapshot = getNetworkStatsUidDetail();
+            final NetworkStats devSnapshot = getNetworkStatsSummary();
 
-            // record uid stats, folding in tethering stats
-            uidSnapshot = mNetworkManager.getNetworkStatsUidDetail(UID_ALL);
-            uidSnapshot.combineAllValues(tetherSnapshot);
-            performUidPollLocked(uidSnapshot, currentTime);
-
-            // record dev network stats
-            networkDevSnapshot = mNetworkManager.getNetworkStatsSummary();
-            performNetworkDevPollLocked(networkDevSnapshot, currentTime);
-
-            // record xt network stats
-            networkXtSnapshot = computeNetworkXtSnapshotFromUid(uidSnapshot);
-            performNetworkXtPollLocked(networkXtSnapshot, currentTime);
+            mDevRecorder.recordSnapshotLocked(devSnapshot, mActiveIfaces, currentTime);
+            mUidRecorder.recordSnapshotLocked(uidSnapshot, mActiveIfaces, currentTime);
+            mUidTagRecorder.recordSnapshotLocked(uidSnapshot, mActiveIfaces, currentTime);
 
         } catch (IllegalStateException e) {
             Log.wtf(TAG, "problem reading network stats", e);
@@ -860,26 +763,19 @@
             return;
         }
 
-        // persist when enough network data has occurred
-        final long persistNetworkDevDelta = computeStatsDelta(
-                mLastPersistNetworkDevSnapshot, networkDevSnapshot, true, "devp").getTotalBytes();
-        final long persistNetworkXtDelta = computeStatsDelta(
-                mLastPersistNetworkXtSnapshot, networkXtSnapshot, true, "xtp").getTotalBytes();
-        final boolean networkOverThreshold = persistNetworkDevDelta > threshold
-                || persistNetworkXtDelta > threshold;
-        if (persistForce || (persistNetwork && networkOverThreshold)) {
-            writeNetworkDevStatsLocked();
-            writeNetworkXtStatsLocked();
-            mLastPersistNetworkDevSnapshot = networkDevSnapshot;
-            mLastPersistNetworkXtSnapshot = networkXtSnapshot;
-        }
-
-        // persist when enough uid data has occurred
-        final long persistUidDelta = computeStatsDelta(
-                mLastPersistUidSnapshot, uidSnapshot, true, "uidp").getTotalBytes();
-        if (persistForce || (persistUid && persistUidDelta > threshold)) {
-            writeUidStatsLocked();
-            mLastPersistUidSnapshot = uidSnapshot;
+        // persist any pending data depending on requested flags
+        if (persistForce) {
+            mDevRecorder.forcePersistLocked(currentTime);
+            mUidRecorder.forcePersistLocked(currentTime);
+            mUidTagRecorder.forcePersistLocked(currentTime);
+        } else {
+            if (persistNetwork) {
+                mDevRecorder.maybePersistLocked(currentTime);
+            }
+            if (persistUid) {
+                mUidRecorder.maybePersistLocked(currentTime);
+                mUidTagRecorder.maybePersistLocked(currentTime);
+            }
         }
 
         if (LOGV) {
@@ -887,9 +783,9 @@
             Slog.v(TAG, "performPollLocked() took " + duration + "ms");
         }
 
-        if (ENABLE_SAMPLE_AFTER_POLL) {
+        if (mSettings.getSampleEnabled()) {
             // sample stats after each full poll
-            performSample();
+            performSampleLocked();
         }
 
         // finally, dispatch updated event to any listeners
@@ -899,511 +795,58 @@
     }
 
     /**
-     * Update {@link #mNetworkDevStats} historical usage.
-     */
-    private void performNetworkDevPollLocked(NetworkStats networkDevSnapshot, long currentTime) {
-        final HashSet<String> unknownIface = Sets.newHashSet();
-
-        final NetworkStats delta = computeStatsDelta(
-                mLastPollNetworkDevSnapshot, networkDevSnapshot, false, "dev");
-        final long timeStart = currentTime - delta.getElapsedRealtime();
-
-        NetworkStats.Entry entry = null;
-        for (int i = 0; i < delta.size(); i++) {
-            entry = delta.getValues(i, entry);
-            final NetworkIdentitySet ident = mActiveIfaces.get(entry.iface);
-            if (ident == null) {
-                unknownIface.add(entry.iface);
-                continue;
-            }
-
-            final NetworkStatsHistory history = findOrCreateNetworkDevStatsLocked(ident);
-            history.recordData(timeStart, currentTime, entry);
-        }
-
-        mLastPollNetworkDevSnapshot = networkDevSnapshot;
-
-        if (LOGD && unknownIface.size() > 0) {
-            Slog.w(TAG, "unknown dev interfaces " + unknownIface + ", ignoring those stats");
-        }
-    }
-
-    /**
-     * Update {@link #mNetworkXtStats} historical usage.
-     */
-    private void performNetworkXtPollLocked(NetworkStats networkXtSnapshot, long currentTime) {
-        final HashSet<String> unknownIface = Sets.newHashSet();
-
-        final NetworkStats delta = computeStatsDelta(
-                mLastPollNetworkXtSnapshot, networkXtSnapshot, false, "xt");
-        final long timeStart = currentTime - delta.getElapsedRealtime();
-
-        NetworkStats.Entry entry = null;
-        for (int i = 0; i < delta.size(); i++) {
-            entry = delta.getValues(i, entry);
-            final NetworkIdentitySet ident = mActiveIfaces.get(entry.iface);
-            if (ident == null) {
-                unknownIface.add(entry.iface);
-                continue;
-            }
-
-            final NetworkStatsHistory history = findOrCreateNetworkXtStatsLocked(ident);
-            history.recordData(timeStart, currentTime, entry);
-        }
-
-        mLastPollNetworkXtSnapshot = networkXtSnapshot;
-
-        if (LOGD && unknownIface.size() > 0) {
-            Slog.w(TAG, "unknown xt interfaces " + unknownIface + ", ignoring those stats");
-        }
-    }
-
-    /**
-     * Update {@link #mUidStats} historical usage.
-     */
-    private void performUidPollLocked(NetworkStats uidSnapshot, long currentTime) {
-        ensureUidStatsLoadedLocked();
-
-        final NetworkStats delta = computeStatsDelta(
-                mLastPollUidSnapshot, uidSnapshot, false, "uid");
-        final NetworkStats operationsDelta = computeStatsDelta(
-                mLastPollOperationsSnapshot, mOperations, false, "uidop");
-        final long timeStart = currentTime - delta.getElapsedRealtime();
-
-        NetworkStats.Entry entry = null;
-        NetworkStats.Entry operationsEntry = null;
-        for (int i = 0; i < delta.size(); i++) {
-            entry = delta.getValues(i, entry);
-            final NetworkIdentitySet ident = mActiveIfaces.get(entry.iface);
-            if (ident == null) {
-                if (entry.rxBytes > 0 || entry.rxPackets > 0 || entry.txBytes > 0
-                        || entry.txPackets > 0) {
-                    Log.w(TAG, "dropping UID delta from unknown iface: " + entry);
-                }
-                continue;
-            }
-
-            // splice in operation counts since last poll
-            final int j = operationsDelta.findIndex(IFACE_ALL, entry.uid, entry.set, entry.tag);
-            if (j != -1) {
-                operationsEntry = operationsDelta.getValues(j, operationsEntry);
-                entry.operations = operationsEntry.operations;
-            }
-
-            final NetworkStatsHistory history = findOrCreateUidStatsLocked(
-                    ident, entry.uid, entry.set, entry.tag);
-            history.recordData(timeStart, currentTime, entry);
-        }
-
-        mLastPollUidSnapshot = uidSnapshot;
-        mLastPollOperationsSnapshot = mOperations.clone();
-    }
-
-    /**
      * Sample recent statistics summary into {@link EventLog}.
      */
-    private void performSample() {
-        final long largestBucketSize = Math.max(
-                mSettings.getNetworkBucketDuration(), mSettings.getUidBucketDuration());
-
-        // take sample as atomic buckets
-        final long now = mTime.hasCache() ? mTime.currentTimeMillis() : System.currentTimeMillis();
-        final long end = now - (now % largestBucketSize) + largestBucketSize;
-        final long start = end - largestBucketSize;
-
+    private void performSampleLocked() {
+        // TODO: migrate trustedtime fixes to separate binary log events
         final long trustedTime = mTime.hasCache() ? mTime.currentTimeMillis() : -1;
-        long devHistoryStart = Long.MAX_VALUE;
 
-        NetworkTemplate template = null;
-        NetworkStats.Entry devTotal = null;
-        NetworkStats.Entry xtTotal = null;
-        NetworkStats.Entry uidTotal = null;
+        NetworkTemplate template;
+        NetworkStats.Entry devTotal;
+        NetworkStats.Entry xtTotal;
+        NetworkStats.Entry uidTotal;
 
         // collect mobile sample
         template = buildTemplateMobileAll(getActiveSubscriberId(mContext));
-        devTotal = getSummaryForNetworkDev(template, start, end).getTotal(devTotal);
-        devHistoryStart = getHistoryStartLocked(template, mNetworkDevStats);
-        xtTotal = getSummaryForNetworkXt(template, start, end).getTotal(xtTotal);
-        uidTotal = getSummaryForAllUid(template, start, end, false).getTotal(uidTotal);
+        devTotal = mDevRecorder.getTotalSinceBootLocked(template);
+        xtTotal = new NetworkStats.Entry();
+        uidTotal = mUidRecorder.getTotalSinceBootLocked(template);
 
         EventLogTags.writeNetstatsMobileSample(
                 devTotal.rxBytes, devTotal.rxPackets, devTotal.txBytes, devTotal.txPackets,
                 xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets,
                 uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets,
-                trustedTime, devHistoryStart);
+                trustedTime);
 
         // collect wifi sample
         template = buildTemplateWifi();
-        devTotal = getSummaryForNetworkDev(template, start, end).getTotal(devTotal);
-        devHistoryStart = getHistoryStartLocked(template, mNetworkDevStats);
-        xtTotal = getSummaryForNetworkXt(template, start, end).getTotal(xtTotal);
-        uidTotal = getSummaryForAllUid(template, start, end, false).getTotal(uidTotal);
+        devTotal = mDevRecorder.getTotalSinceBootLocked(template);
+        xtTotal = new NetworkStats.Entry();
+        uidTotal = mUidRecorder.getTotalSinceBootLocked(template);
+
         EventLogTags.writeNetstatsWifiSample(
                 devTotal.rxBytes, devTotal.rxPackets, devTotal.txBytes, devTotal.txPackets,
                 xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets,
                 uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets,
-                trustedTime, devHistoryStart);
+                trustedTime);
     }
 
     /**
-     * Clean up {@link #mUidStats} after UID is removed.
+     * Clean up {@link #mUidRecorder} after UID is removed.
      */
     private void removeUidLocked(int uid) {
-        ensureUidStatsLoadedLocked();
-
         // perform one last poll before removing
         performPollLocked(FLAG_PERSIST_ALL);
 
-        final ArrayList<UidStatsKey> knownKeys = Lists.newArrayList();
-        knownKeys.addAll(mUidStats.keySet());
-
-        // migrate all UID stats into special "removed" bucket
-        for (UidStatsKey key : knownKeys) {
-            if (key.uid == uid) {
-                // only migrate combined TAG_NONE history
-                if (key.tag == TAG_NONE) {
-                    final NetworkStatsHistory uidHistory = mUidStats.get(key);
-                    final NetworkStatsHistory removedHistory = findOrCreateUidStatsLocked(
-                            key.ident, UID_REMOVED, SET_DEFAULT, TAG_NONE);
-                    removedHistory.recordEntireHistory(uidHistory);
-                }
-                mUidStats.remove(key);
-            }
-        }
-
-        // clear UID from current stats snapshot
-        if (mLastPollUidSnapshot != null) {
-            mLastPollUidSnapshot = mLastPollUidSnapshot.withoutUid(uid);
-            mLastPollNetworkXtSnapshot = computeNetworkXtSnapshotFromUid(mLastPollUidSnapshot);
-        }
+        mUidRecorder.removeUidLocked(uid);
+        mUidTagRecorder.removeUidLocked(uid);
 
         // clear kernel stats associated with UID
         resetKernelUidStats(uid);
-
-        // since this was radical rewrite, push to disk
-        writeUidStatsLocked();
-    }
-
-    private NetworkStatsHistory findOrCreateNetworkXtStatsLocked(NetworkIdentitySet ident) {
-        return findOrCreateNetworkStatsLocked(ident, mNetworkXtStats);
-    }
-
-    private NetworkStatsHistory findOrCreateNetworkDevStatsLocked(NetworkIdentitySet ident) {
-        return findOrCreateNetworkStatsLocked(ident, mNetworkDevStats);
-    }
-
-    private NetworkStatsHistory findOrCreateNetworkStatsLocked(
-            NetworkIdentitySet ident, HashMap<NetworkIdentitySet, NetworkStatsHistory> source) {
-        final NetworkStatsHistory existing = source.get(ident);
-
-        // update when no existing, or when bucket duration changed
-        final long bucketDuration = mSettings.getNetworkBucketDuration();
-        NetworkStatsHistory updated = null;
-        if (existing == null) {
-            updated = new NetworkStatsHistory(bucketDuration, 10);
-        } else if (existing.getBucketDuration() != bucketDuration) {
-            updated = new NetworkStatsHistory(
-                    bucketDuration, estimateResizeBuckets(existing, bucketDuration));
-            updated.recordEntireHistory(existing);
-        }
-
-        if (updated != null) {
-            source.put(ident, updated);
-            return updated;
-        } else {
-            return existing;
-        }
-    }
-
-    private NetworkStatsHistory findOrCreateUidStatsLocked(
-            NetworkIdentitySet ident, int uid, int set, int tag) {
-        ensureUidStatsLoadedLocked();
-
-        final UidStatsKey key = new UidStatsKey(ident, uid, set, tag);
-        final NetworkStatsHistory existing = mUidStats.get(key);
-
-        // update when no existing, or when bucket duration changed
-        final long bucketDuration = mSettings.getUidBucketDuration();
-        NetworkStatsHistory updated = null;
-        if (existing == null) {
-            updated = new NetworkStatsHistory(bucketDuration, 10);
-        } else if (existing.getBucketDuration() != bucketDuration) {
-            updated = new NetworkStatsHistory(
-                    bucketDuration, estimateResizeBuckets(existing, bucketDuration));
-            updated.recordEntireHistory(existing);
-        }
-
-        if (updated != null) {
-            mUidStats.put(key, updated);
-            return updated;
-        } else {
-            return existing;
-        }
-    }
-
-    private void readNetworkDevStatsLocked() {
-        if (LOGV) Slog.v(TAG, "readNetworkDevStatsLocked()");
-        readNetworkStats(mNetworkDevFile, mNetworkDevStats);
-    }
-
-    private void readNetworkXtStatsLocked() {
-        if (LOGV) Slog.v(TAG, "readNetworkXtStatsLocked()");
-        readNetworkStats(mNetworkXtFile, mNetworkXtStats);
-    }
-
-    private static void readNetworkStats(
-            AtomicFile inputFile, HashMap<NetworkIdentitySet, NetworkStatsHistory> output) {
-        // clear any existing stats and read from disk
-        output.clear();
-
-        DataInputStream in = null;
-        try {
-            in = new DataInputStream(new BufferedInputStream(inputFile.openRead()));
-
-            // verify file magic header intact
-            final int magic = in.readInt();
-            if (magic != FILE_MAGIC) {
-                throw new ProtocolException("unexpected magic: " + magic);
-            }
-
-            final int version = in.readInt();
-            switch (version) {
-                case VERSION_NETWORK_INIT: {
-                    // network := size *(NetworkIdentitySet NetworkStatsHistory)
-                    final int size = in.readInt();
-                    for (int i = 0; i < size; i++) {
-                        final NetworkIdentitySet ident = new NetworkIdentitySet(in);
-                        final NetworkStatsHistory history = new NetworkStatsHistory(in);
-                        output.put(ident, history);
-                    }
-                    break;
-                }
-                default: {
-                    throw new ProtocolException("unexpected version: " + version);
-                }
-            }
-        } catch (FileNotFoundException e) {
-            // missing stats is okay, probably first boot
-        } catch (IOException e) {
-            Log.wtf(TAG, "problem reading network stats", e);
-        } finally {
-            IoUtils.closeQuietly(in);
-        }
-    }
-
-    private void ensureUidStatsLoadedLocked() {
-        if (!mUidStatsLoaded) {
-            readUidStatsLocked();
-            mUidStatsLoaded = true;
-        }
-    }
-
-    private void readUidStatsLocked() {
-        if (LOGV) Slog.v(TAG, "readUidStatsLocked()");
-
-        // clear any existing stats and read from disk
-        mUidStats.clear();
-
-        DataInputStream in = null;
-        try {
-            in = new DataInputStream(new BufferedInputStream(mUidFile.openRead()));
-
-            // verify file magic header intact
-            final int magic = in.readInt();
-            if (magic != FILE_MAGIC) {
-                throw new ProtocolException("unexpected magic: " + magic);
-            }
-
-            final int version = in.readInt();
-            switch (version) {
-                case VERSION_UID_INIT: {
-                    // uid := size *(UID NetworkStatsHistory)
-
-                    // drop this data version, since we don't have a good
-                    // mapping into NetworkIdentitySet.
-                    break;
-                }
-                case VERSION_UID_WITH_IDENT: {
-                    // uid := size *(NetworkIdentitySet size *(UID NetworkStatsHistory))
-
-                    // drop this data version, since this version only existed
-                    // for a short time.
-                    break;
-                }
-                case VERSION_UID_WITH_TAG:
-                case VERSION_UID_WITH_SET: {
-                    // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory))
-                    final int identSize = in.readInt();
-                    for (int i = 0; i < identSize; i++) {
-                        final NetworkIdentitySet ident = new NetworkIdentitySet(in);
-
-                        final int size = in.readInt();
-                        for (int j = 0; j < size; j++) {
-                            final int uid = in.readInt();
-                            final int set = (version >= VERSION_UID_WITH_SET) ? in.readInt()
-                                    : SET_DEFAULT;
-                            final int tag = in.readInt();
-
-                            final UidStatsKey key = new UidStatsKey(ident, uid, set, tag);
-                            final NetworkStatsHistory history = new NetworkStatsHistory(in);
-                            mUidStats.put(key, history);
-                        }
-                    }
-                    break;
-                }
-                default: {
-                    throw new ProtocolException("unexpected version: " + version);
-                }
-            }
-        } catch (FileNotFoundException e) {
-            // missing stats is okay, probably first boot
-        } catch (IOException e) {
-            Log.wtf(TAG, "problem reading uid stats", e);
-        } finally {
-            IoUtils.closeQuietly(in);
-        }
-    }
-
-    private void writeNetworkDevStatsLocked() {
-        if (LOGV) Slog.v(TAG, "writeNetworkDevStatsLocked()");
-        writeNetworkStats(mNetworkDevStats, mNetworkDevFile);
-    }
-
-    private void writeNetworkXtStatsLocked() {
-        if (LOGV) Slog.v(TAG, "writeNetworkXtStatsLocked()");
-        writeNetworkStats(mNetworkXtStats, mNetworkXtFile);
-    }
-
-    private void writeNetworkStats(
-            HashMap<NetworkIdentitySet, NetworkStatsHistory> input, AtomicFile outputFile) {
-        // TODO: consider duplicating stats and releasing lock while writing
-
-        // trim any history beyond max
-        if (mTime.hasCache()) {
-            final long systemCurrentTime = System.currentTimeMillis();
-            final long trustedCurrentTime = mTime.currentTimeMillis();
-
-            final long currentTime = Math.min(systemCurrentTime, trustedCurrentTime);
-            final long maxHistory = mSettings.getNetworkMaxHistory();
-
-            for (NetworkStatsHistory history : input.values()) {
-                final int beforeSize = history.size();
-                history.removeBucketsBefore(currentTime - maxHistory);
-                final int afterSize = history.size();
-
-                if (beforeSize > 24 && afterSize < beforeSize / 2) {
-                    // yikes, dropping more than half of significant history
-                    final StringBuilder builder = new StringBuilder();
-                    builder.append("yikes, dropping more than half of history").append('\n');
-                    builder.append("systemCurrentTime=").append(systemCurrentTime).append('\n');
-                    builder.append("trustedCurrentTime=").append(trustedCurrentTime).append('\n');
-                    builder.append("maxHistory=").append(maxHistory).append('\n');
-                    builder.append("beforeSize=").append(beforeSize).append('\n');
-                    builder.append("afterSize=").append(afterSize).append('\n');
-                    mDropBox.addText(TAG_NETSTATS_ERROR, builder.toString());
-                }
-            }
-        }
-
-        FileOutputStream fos = null;
-        try {
-            fos = outputFile.startWrite();
-            final DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fos));
-
-            out.writeInt(FILE_MAGIC);
-            out.writeInt(VERSION_NETWORK_INIT);
-
-            out.writeInt(input.size());
-            for (NetworkIdentitySet ident : input.keySet()) {
-                final NetworkStatsHistory history = input.get(ident);
-                ident.writeToStream(out);
-                history.writeToStream(out);
-            }
-
-            out.flush();
-            outputFile.finishWrite(fos);
-        } catch (IOException e) {
-            Log.wtf(TAG, "problem writing stats", e);
-            if (fos != null) {
-                outputFile.failWrite(fos);
-            }
-        }
-    }
-
-    private void writeUidStatsLocked() {
-        if (LOGV) Slog.v(TAG, "writeUidStatsLocked()");
-
-        if (!mUidStatsLoaded) {
-            Slog.w(TAG, "asked to write UID stats when not loaded; skipping");
-            return;
-        }
-
-        // TODO: consider duplicating stats and releasing lock while writing
-
-        // trim any history beyond max
-        if (mTime.hasCache()) {
-            final long currentTime = Math.min(
-                    System.currentTimeMillis(), mTime.currentTimeMillis());
-            final long maxUidHistory = mSettings.getUidMaxHistory();
-            final long maxTagHistory = mSettings.getTagMaxHistory();
-            for (UidStatsKey key : mUidStats.keySet()) {
-                final NetworkStatsHistory history = mUidStats.get(key);
-
-                // detailed tags are trimmed sooner than summary in TAG_NONE
-                if (key.tag == TAG_NONE) {
-                    history.removeBucketsBefore(currentTime - maxUidHistory);
-                } else {
-                    history.removeBucketsBefore(currentTime - maxTagHistory);
-                }
-            }
-        }
-
-        // build UidStatsKey lists grouped by ident
-        final HashMap<NetworkIdentitySet, ArrayList<UidStatsKey>> keysByIdent = Maps.newHashMap();
-        for (UidStatsKey key : mUidStats.keySet()) {
-            ArrayList<UidStatsKey> keys = keysByIdent.get(key.ident);
-            if (keys == null) {
-                keys = Lists.newArrayList();
-                keysByIdent.put(key.ident, keys);
-            }
-            keys.add(key);
-        }
-
-        FileOutputStream fos = null;
-        try {
-            fos = mUidFile.startWrite();
-            final DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fos));
-
-            out.writeInt(FILE_MAGIC);
-            out.writeInt(VERSION_UID_WITH_SET);
-
-            out.writeInt(keysByIdent.size());
-            for (NetworkIdentitySet ident : keysByIdent.keySet()) {
-                final ArrayList<UidStatsKey> keys = keysByIdent.get(ident);
-                ident.writeToStream(out);
-
-                out.writeInt(keys.size());
-                for (UidStatsKey key : keys) {
-                    final NetworkStatsHistory history = mUidStats.get(key);
-                    out.writeInt(key.uid);
-                    out.writeInt(key.set);
-                    out.writeInt(key.tag);
-                    history.writeToStream(out);
-                }
-            }
-
-            out.flush();
-            mUidFile.finishWrite(fos);
-        } catch (IOException e) {
-            Log.wtf(TAG, "problem writing stats", e);
-            if (fos != null) {
-                mUidFile.failWrite(fos);
-            }
-        }
     }
 
     @Override
-    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
         mContext.enforceCallingOrSelfPermission(DUMP, TAG);
 
         final HashSet<String> argSet = new HashSet<String>();
@@ -1411,187 +854,68 @@
             argSet.add(arg);
         }
 
-        final boolean fullHistory = argSet.contains("full");
+        // usage: dumpsys netstats --full --uid --tag
+        final boolean poll = argSet.contains("--poll") || argSet.contains("poll");
+        final boolean fullHistory = argSet.contains("--full") || argSet.contains("full");
+        final boolean includeUid = argSet.contains("--uid") || argSet.contains("detail");
+        final boolean includeTag = argSet.contains("--tag") || argSet.contains("detail");
+
+        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
 
         synchronized (mStatsLock) {
-            // TODO: remove this testing code, since it corrupts stats
-            if (argSet.contains("generate")) {
-                generateRandomLocked(args);
-                pw.println("Generated stub stats");
-                return;
-            }
-
-            if (argSet.contains("poll")) {
+            if (poll) {
                 performPollLocked(FLAG_PERSIST_ALL | FLAG_PERSIST_FORCE);
                 pw.println("Forced poll");
                 return;
             }
 
             pw.println("Active interfaces:");
+            pw.increaseIndent();
             for (String iface : mActiveIfaces.keySet()) {
                 final NetworkIdentitySet ident = mActiveIfaces.get(iface);
-                pw.print("  iface="); pw.print(iface);
+                pw.print("iface="); pw.print(iface);
                 pw.print(" ident="); pw.println(ident.toString());
             }
+            pw.decreaseIndent();
 
-            pw.println("Known historical dev stats:");
-            for (NetworkIdentitySet ident : mNetworkDevStats.keySet()) {
-                final NetworkStatsHistory history = mNetworkDevStats.get(ident);
-                pw.print("  ident="); pw.println(ident.toString());
-                history.dump("  ", pw, fullHistory);
+            pw.println("Dev stats:");
+            pw.increaseIndent();
+            mDevRecorder.dumpLocked(pw, fullHistory);
+            pw.decreaseIndent();
+
+            if (includeUid) {
+                pw.println("UID stats:");
+                pw.increaseIndent();
+                mUidRecorder.dumpLocked(pw, fullHistory);
+                pw.decreaseIndent();
             }
 
-            pw.println("Known historical xt stats:");
-            for (NetworkIdentitySet ident : mNetworkXtStats.keySet()) {
-                final NetworkStatsHistory history = mNetworkXtStats.get(ident);
-                pw.print("  ident="); pw.println(ident.toString());
-                history.dump("  ", pw, fullHistory);
-            }
-
-            if (argSet.contains("detail")) {
-                // since explicitly requested with argument, we're okay to load
-                // from disk if not already in memory.
-                ensureUidStatsLoadedLocked();
-
-                final ArrayList<UidStatsKey> keys = Lists.newArrayList();
-                keys.addAll(mUidStats.keySet());
-                Collections.sort(keys);
-
-                pw.println("Detailed UID stats:");
-                for (UidStatsKey key : keys) {
-                    pw.print("  ident="); pw.print(key.ident.toString());
-                    pw.print(" uid="); pw.print(key.uid);
-                    pw.print(" set="); pw.print(NetworkStats.setToString(key.set));
-                    pw.print(" tag="); pw.println(NetworkStats.tagToString(key.tag));
-
-                    final NetworkStatsHistory history = mUidStats.get(key);
-                    history.dump("    ", pw, fullHistory);
-                }
+            if (includeTag) {
+                pw.println("UID tag stats:");
+                pw.increaseIndent();
+                mUidTagRecorder.dumpLocked(pw, fullHistory);
+                pw.decreaseIndent();
             }
         }
     }
 
+    private NetworkStats getNetworkStatsSummary() throws RemoteException {
+        return mNetworkManager.getNetworkStatsSummary();
+    }
+
     /**
-     * @deprecated only for temporary testing
+     * Return snapshot of current UID statistics, including any
+     * {@link TrafficStats#UID_TETHERING} and {@link #mUidOperations} values.
      */
-    @Deprecated
-    private void generateRandomLocked(String[] args) {
-        final long totalBytes = Long.parseLong(args[1]);
-        final long totalTime = Long.parseLong(args[2]);
-        
-        final PackageManager pm = mContext.getPackageManager();
-        final ArrayList<Integer> specialUidList = Lists.newArrayList();
-        for (int i = 3; i < args.length; i++) {
-            try {
-                specialUidList.add(pm.getApplicationInfo(args[i], 0).uid);
-            } catch (NameNotFoundException e) {
-                throw new RuntimeException(e);
-            }
-        }
+    private NetworkStats getNetworkStatsUidDetail() throws RemoteException {
+        final NetworkStats uidSnapshot = mNetworkManager.getNetworkStatsUidDetail(UID_ALL);
 
-        final HashSet<Integer> otherUidSet = Sets.newHashSet();
-        for (ApplicationInfo info : pm.getInstalledApplications(0)) {
-            if (pm.checkPermission(android.Manifest.permission.INTERNET, info.packageName)
-                    == PackageManager.PERMISSION_GRANTED && !specialUidList.contains(info.uid)) {
-                otherUidSet.add(info.uid);
-            }
-        }
+        // fold tethering stats and operations into uid snapshot
+        final NetworkStats tetherSnapshot = getNetworkStatsTethering();
+        uidSnapshot.combineAllValues(tetherSnapshot);
+        uidSnapshot.combineAllValues(mUidOperations);
 
-        final ArrayList<Integer> otherUidList = new ArrayList<Integer>(otherUidSet);
-
-        final long end = System.currentTimeMillis();
-        final long start = end - totalTime;
-
-        mNetworkDevStats.clear();
-        mNetworkXtStats.clear();
-        mUidStats.clear();
-
-        final Random r = new Random();
-        for (NetworkIdentitySet ident : mActiveIfaces.values()) {
-            final NetworkStatsHistory devHistory = findOrCreateNetworkDevStatsLocked(ident);
-            final NetworkStatsHistory xtHistory = findOrCreateNetworkXtStatsLocked(ident);
-
-            final ArrayList<Integer> uidList = new ArrayList<Integer>();
-            uidList.addAll(specialUidList);
-
-            if (uidList.size() == 0) {
-                Collections.shuffle(otherUidList);
-                uidList.addAll(otherUidList);
-            }
-
-            boolean first = true;
-            long remainingBytes = totalBytes;
-            for (int uid : uidList) {
-                final NetworkStatsHistory defaultHistory = findOrCreateUidStatsLocked(
-                        ident, uid, SET_DEFAULT, TAG_NONE);
-                final NetworkStatsHistory foregroundHistory = findOrCreateUidStatsLocked(
-                        ident, uid, SET_FOREGROUND, TAG_NONE);
-
-                final long uidBytes = totalBytes / uidList.size();
-
-                final float fractionDefault = r.nextFloat();
-                final long defaultBytes = (long) (uidBytes * fractionDefault);
-                final long foregroundBytes = (long) (uidBytes * (1 - fractionDefault));
-
-                defaultHistory.generateRandom(start, end, defaultBytes);
-                foregroundHistory.generateRandom(start, end, foregroundBytes);
-
-                if (first) {
-                    final long bumpTime = (start + end) / 2;
-                    defaultHistory.recordData(
-                            bumpTime, bumpTime + DAY_IN_MILLIS, 200 * MB_IN_BYTES, 0);
-                    first = false;
-                }
-
-                devHistory.recordEntireHistory(defaultHistory);
-                devHistory.recordEntireHistory(foregroundHistory);
-                xtHistory.recordEntireHistory(defaultHistory);
-                xtHistory.recordEntireHistory(foregroundHistory);
-            }
-        }
-    }
-
-    private StatsObserver mStatsObserver = new StatsObserver();
-
-    private class StatsObserver implements NonMonotonicObserver {
-        private String mCurrentType;
-
-        public void setCurrentType(String type) {
-            mCurrentType = type;
-        }
-
-        /** {@inheritDoc} */
-        public void foundNonMonotonic(
-                NetworkStats left, int leftIndex, NetworkStats right, int rightIndex) {
-            Log.w(TAG, "found non-monotonic values; saving to dropbox");
-
-            // record error for debugging
-            final StringBuilder builder = new StringBuilder();
-            builder.append("found non-monotonic " + mCurrentType + " values at left[" + leftIndex
-                    + "] - right[" + rightIndex + "]\n");
-            builder.append("left=").append(left).append('\n');
-            builder.append("right=").append(right).append('\n');
-            mDropBox.addText(TAG_NETSTATS_ERROR, builder.toString());
-        }
-    }
-
-    /**
-     * Return the delta between two {@link NetworkStats} snapshots, where {@code
-     * before} can be {@code null}.
-     */
-    private NetworkStats computeStatsDelta(
-            NetworkStats before, NetworkStats current, boolean collectStale, String type) {
-        if (before != null) {
-            mStatsObserver.setCurrentType(type);
-            return NetworkStats.subtract(current, before, mStatsObserver);
-        } else if (collectStale) {
-            // caller is okay collecting stale stats for first call.
-            return current;
-        } else {
-            // this is first snapshot; to prevent from double-counting we only
-            // observe traffic occuring between known snapshots.
-            return new NetworkStats(0L, 10);
-        }
+        return uidSnapshot;
     }
 
     /**
@@ -1608,35 +932,6 @@
         }
     }
 
-    private static NetworkStats computeNetworkXtSnapshotFromUid(NetworkStats uidSnapshot) {
-        return uidSnapshot.groupedByIface();
-    }
-
-    private int estimateNetworkBuckets() {
-        return (int) (mSettings.getNetworkMaxHistory() / mSettings.getNetworkBucketDuration());
-    }
-
-    private int estimateUidBuckets() {
-        return (int) (mSettings.getUidMaxHistory() / mSettings.getUidBucketDuration());
-    }
-
-    private static int estimateResizeBuckets(NetworkStatsHistory existing, long newBucketDuration) {
-        return (int) (existing.size() * existing.getBucketDuration() / newBucketDuration);
-    }
-
-    /**
-     * Test if given {@link NetworkTemplate} matches any {@link NetworkIdentity}
-     * in the given {@link NetworkIdentitySet}.
-     */
-    private static boolean templateMatches(NetworkTemplate template, NetworkIdentitySet identSet) {
-        for (NetworkIdentity ident : identSet) {
-            if (template.matches(ident)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     private Handler.Callback mHandlerCallback = new Handler.Callback() {
         /** {@inheritDoc} */
         public boolean handleMessage(Message msg) {
@@ -1672,40 +967,22 @@
         }
     }
 
-    /**
-     * Key uniquely identifying a {@link NetworkStatsHistory} for a UID.
-     */
-    private static class UidStatsKey implements Comparable<UidStatsKey> {
-        public final NetworkIdentitySet ident;
-        public final int uid;
-        public final int set;
-        public final int tag;
-
-        public UidStatsKey(NetworkIdentitySet ident, int uid, int set, int tag) {
-            this.ident = ident;
-            this.uid = uid;
-            this.set = set;
-            this.tag = tag;
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hashCode(ident, uid, set, tag);
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (obj instanceof UidStatsKey) {
-                final UidStatsKey key = (UidStatsKey) obj;
-                return Objects.equal(ident, key.ident) && uid == key.uid && set == key.set
-                        && tag == key.tag;
-            }
-            return false;
-        }
-
+    private class DropBoxNonMonotonicObserver implements NonMonotonicObserver<String> {
         /** {@inheritDoc} */
-        public int compareTo(UidStatsKey another) {
-            return Integer.compare(uid, another.uid);
+        public void foundNonMonotonic(NetworkStats left, int leftIndex, NetworkStats right,
+                int rightIndex, String cookie) {
+            Log.w(TAG, "found non-monotonic values; saving to dropbox");
+
+            // record error for debugging
+            final StringBuilder builder = new StringBuilder();
+            builder.append("found non-monotonic " + cookie + " values at left[" + leftIndex
+                    + "] - right[" + rightIndex + "]\n");
+            builder.append("left=").append(left).append('\n');
+            builder.append("right=").append(right).append('\n');
+
+            final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
+                    Context.DROPBOX_SERVICE);
+            dropBox.addText(TAG_NETSTATS_ERROR, builder.toString());
         }
     }
 
@@ -1731,26 +1008,35 @@
         public long getPollInterval() {
             return getSecureLong(NETSTATS_POLL_INTERVAL, 30 * MINUTE_IN_MILLIS);
         }
-        public long getPersistThreshold() {
-            return getSecureLong(NETSTATS_PERSIST_THRESHOLD, 2 * MB_IN_BYTES);
-        }
-        public long getNetworkBucketDuration() {
-            return getSecureLong(NETSTATS_NETWORK_BUCKET_DURATION, HOUR_IN_MILLIS);
-        }
-        public long getNetworkMaxHistory() {
-            return getSecureLong(NETSTATS_NETWORK_MAX_HISTORY, 90 * DAY_IN_MILLIS);
-        }
-        public long getUidBucketDuration() {
-            return getSecureLong(NETSTATS_UID_BUCKET_DURATION, 2 * HOUR_IN_MILLIS);
-        }
-        public long getUidMaxHistory() {
-            return getSecureLong(NETSTATS_UID_MAX_HISTORY, 90 * DAY_IN_MILLIS);
-        }
-        public long getTagMaxHistory() {
-            return getSecureLong(NETSTATS_TAG_MAX_HISTORY, 30 * DAY_IN_MILLIS);
-        }
         public long getTimeCacheMaxAge() {
-            return DAY_IN_MILLIS;
+            return getSecureLong(NETSTATS_TIME_CACHE_MAX_AGE, DAY_IN_MILLIS);
+        }
+        public long getGlobalAlertBytes() {
+            return getSecureLong(NETSTATS_GLOBAL_ALERT_BYTES, 2 * MB_IN_BYTES);
+        }
+        public boolean getSampleEnabled() {
+            return getSecureBoolean(NETSTATS_SAMPLE_ENABLED, true);
+        }
+
+        public Config getDevConfig() {
+            return new Config(getSecureLong(NETSTATS_DEV_BUCKET_DURATION, HOUR_IN_MILLIS),
+                    getSecureLong(NETSTATS_DEV_PERSIST_BYTES, 2 * MB_IN_BYTES),
+                    getSecureLong(NETSTATS_DEV_ROTATE_AGE, 15 * DAY_IN_MILLIS),
+                    getSecureLong(NETSTATS_DEV_DELETE_AGE, 90 * DAY_IN_MILLIS));
+        }
+
+        public Config getUidConfig() {
+            return new Config(getSecureLong(NETSTATS_UID_BUCKET_DURATION, 2 * HOUR_IN_MILLIS),
+                    getSecureLong(NETSTATS_UID_PERSIST_BYTES, 2 * MB_IN_BYTES),
+                    getSecureLong(NETSTATS_UID_ROTATE_AGE, 15 * DAY_IN_MILLIS),
+                    getSecureLong(NETSTATS_UID_DELETE_AGE, 90 * DAY_IN_MILLIS));
+        }
+
+        public Config getUidTagConfig() {
+            return new Config(getSecureLong(NETSTATS_UID_BUCKET_DURATION, 2 * HOUR_IN_MILLIS),
+                    getSecureLong(NETSTATS_UID_PERSIST_BYTES, 2 * MB_IN_BYTES),
+                    getSecureLong(NETSTATS_UID_ROTATE_AGE, 5 * DAY_IN_MILLIS),
+                    getSecureLong(NETSTATS_UID_DELETE_AGE, 15 * DAY_IN_MILLIS));
         }
     }
 }
diff --git a/services/java/com/android/server/wm/WindowManagerService.java b/services/java/com/android/server/wm/WindowManagerService.java
index 24b6064..19d94a1 100644
--- a/services/java/com/android/server/wm/WindowManagerService.java
+++ b/services/java/com/android/server/wm/WindowManagerService.java
@@ -2543,8 +2543,12 @@
             if (win == null) {
                 return 0;
             }
-            win.mRequestedWidth = requestedWidth;
-            win.mRequestedHeight = requestedHeight;
+            if (win.mRequestedWidth != requestedWidth
+                    || win.mRequestedHeight != requestedHeight) {
+                win.mLayoutNeeded = true;
+                win.mRequestedWidth = requestedWidth;
+                win.mRequestedHeight = requestedHeight;
+            }
             if (attrs != null && seq == win.mSeq) {
                 win.mSystemUiVisibility = systemUiVisibility;
             }
@@ -2565,6 +2569,9 @@
                 }
                 flagChanges = win.mAttrs.flags ^= attrs.flags;
                 attrChanges = win.mAttrs.copyFrom(attrs);
+                if ((attrChanges&WindowManager.LayoutParams.LAYOUT_CHANGED) != 0) {
+                    win.mLayoutNeeded = true;
+                }
             }
 
             if (DEBUG_LAYOUT) Slog.v(TAG, "Relayout " + win + ": " + win.mAttrs);
@@ -7460,12 +7467,13 @@
             // if they want.  (We do the normal layout for INVISIBLE
             // windows, since that means "perform layout as normal,
             // just don't display").
-            if (!gone || !win.mHaveFrame) {
+            if (!gone || !win.mHaveFrame || win.mLayoutNeeded) {
                 if (!win.mLayoutAttached) {
                     if (initial) {
                         //Slog.i(TAG, "Window " + this + " clearing mContentChanged - initial");
                         win.mContentChanged = false;
                     }
+                    win.mLayoutNeeded = false;
                     win.prelayout();
                     mPolicy.layoutWindowLw(win, win.mAttrs, null);
                     win.mLayoutSeq = seq;
@@ -7497,11 +7505,12 @@
                 // windows, since that means "perform layout as normal,
                 // just don't display").
                 if ((win.mViewVisibility != View.GONE && win.mRelayoutCalled)
-                        || !win.mHaveFrame) {
+                        || !win.mHaveFrame || win.mLayoutNeeded) {
                     if (initial) {
                         //Slog.i(TAG, "Window " + this + " clearing mContentChanged - initial");
                         win.mContentChanged = false;
                     }
+                    win.mLayoutNeeded = false;
                     win.prelayout();
                     mPolicy.layoutWindowLw(win, win.mAttrs, win.mAttachedWindow);
                     win.mLayoutSeq = seq;
diff --git a/services/java/com/android/server/wm/WindowState.java b/services/java/com/android/server/wm/WindowState.java
index 9118381..1067cad 100644
--- a/services/java/com/android/server/wm/WindowState.java
+++ b/services/java/com/android/server/wm/WindowState.java
@@ -238,6 +238,12 @@
     // we can give the window focus before waiting for the relayout.
     boolean mRelayoutCalled;
 
+    // If the application has called relayout() with changes that can
+    // impact its window's size, we need to perform a layout pass on it
+    // even if it is not currently visible for layout.  This is set
+    // when in that case until the layout is done.
+    boolean mLayoutNeeded;
+
     // This is set after the Surface has been created but before the
     // window has been drawn.  During this time the surface is hidden.
     boolean mDrawPending;
@@ -1449,7 +1455,7 @@
         return mViewVisibility == View.GONE
                 || !mRelayoutCalled
                 || (atoken == null && mRootToken.hidden)
-                || (atoken != null && atoken.hiddenRequested)
+                || (atoken != null && (atoken.hiddenRequested || atoken.hidden))
                 || mAttachedHidden
                 || mExiting || mDestroying;
     }
@@ -1728,8 +1734,9 @@
                     pw.print(mPolicyVisibilityAfterAnim);
                     pw.print(" mAttachedHidden="); pw.println(mAttachedHidden);
         }
-        if (!mRelayoutCalled) {
-            pw.print(prefix); pw.print("mRelayoutCalled="); pw.println(mRelayoutCalled);
+        if (!mRelayoutCalled || mLayoutNeeded) {
+            pw.print(prefix); pw.print("mRelayoutCalled="); pw.print(mRelayoutCalled);
+                    pw.print(" mLayoutNeeded="); pw.println(mLayoutNeeded);
         }
         if (mSurfaceResized || mSurfaceDestroyDeferred) {
             pw.print(prefix); pw.print("mSurfaceResized="); pw.print(mSurfaceResized);
diff --git a/services/tests/servicestests/res/raw/netstats_uid_v4 b/services/tests/servicestests/res/raw/netstats_uid_v4
new file mode 100644
index 0000000..e75fc1c
--- /dev/null
+++ b/services/tests/servicestests/res/raw/netstats_uid_v4
Binary files differ
diff --git a/services/tests/servicestests/res/raw/netstats_v1 b/services/tests/servicestests/res/raw/netstats_v1
new file mode 100644
index 0000000..e80860a
--- /dev/null
+++ b/services/tests/servicestests/res/raw/netstats_v1
Binary files differ
diff --git a/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java
index 90b5a2e..8f5e77e 100644
--- a/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java
@@ -39,6 +39,7 @@
 import static android.text.format.DateUtils.WEEK_IN_MILLIS;
 import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
 import static org.easymock.EasyMock.anyLong;
+import static org.easymock.EasyMock.aryEq;
 import static org.easymock.EasyMock.capture;
 import static org.easymock.EasyMock.createMock;
 import static org.easymock.EasyMock.eq;
@@ -63,10 +64,12 @@
 import android.telephony.TelephonyManager;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.Suppress;
 import android.util.TrustedTime;
 
 import com.android.server.net.NetworkStatsService;
 import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
+import com.android.server.net.NetworkStatsService.NetworkStatsSettings.Config;
 
 import org.easymock.Capture;
 import org.easymock.EasyMock;
@@ -89,6 +92,10 @@
     private static final String IMSI_1 = "310004";
     private static final String IMSI_2 = "310260";
 
+    private static final long KB_IN_BYTES = 1024;
+    private static final long MB_IN_BYTES = 1024 * KB_IN_BYTES;
+    private static final long GB_IN_BYTES = 1024 * MB_IN_BYTES;
+
     private static NetworkTemplate sTemplateWifi = buildTemplateWifi();
     private static NetworkTemplate sTemplateImsi1 = buildTemplateMobileAll(IMSI_1);
     private static NetworkTemplate sTemplateImsi2 = buildTemplateMobileAll(IMSI_2);
@@ -282,13 +289,6 @@
         mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
         verifyAndReset();
 
-        // talk with zombie service to assert stats have gone; and assert that
-        // we persisted them to file.
-        expectDefaultSettings();
-        replay();
-        assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
-        verifyAndReset();
-
         assertStatsFilesExist(true);
 
         // boot through serviceReady() again
@@ -319,6 +319,8 @@
 
     }
 
+    // TODO: simulate reboot to test bucket resize
+    @Suppress
     public void testStatsBucketResize() throws Exception {
         NetworkStatsHistory history = null;
 
@@ -602,7 +604,6 @@
         assertUidTotal(sTemplateImsi1, UID_RED, 1536L, 12L, 1280L, 10L, 10);
 
         verifyAndReset();
-
     }
 
     public void testSummaryForAllUid() throws Exception {
@@ -755,11 +756,15 @@
         expectDefaultSettings();
         expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .addIfaceValues(TEST_IFACE, 2048L, 16L, 512L, 4L));
-        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
-                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L));
+
+        final NetworkStats uidStats = new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L);
         final String[] tetherIfacePairs = new String[] { TEST_IFACE, "wlan0" };
-        expectNetworkStatsPoll(tetherIfacePairs, new NetworkStats(getElapsedRealtime(), 1)
-                .addValues(TEST_IFACE, UID_TETHERING, SET_DEFAULT, TAG_NONE, 1920L, 14L, 384L, 2L, 0L));
+        final NetworkStats tetherStats = new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_TETHERING, SET_DEFAULT, TAG_NONE, 1920L, 14L, 384L, 2L, 0L);
+
+        expectNetworkStatsUidDetail(uidStats, tetherIfacePairs, tetherStats);
+        expectNetworkStatsPoll();
 
         replay();
         mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
@@ -808,6 +813,9 @@
 
     private void expectNetworkState(NetworkState... state) throws Exception {
         expect(mConnManager.getAllNetworkState()).andReturn(state).atLeastOnce();
+
+        final LinkProperties linkProp = state.length > 0 ? state[0].linkProperties : null;
+        expect(mConnManager.getActiveLinkProperties()).andReturn(linkProp).atLeastOnce();
     }
 
     private void expectNetworkStatsSummary(NetworkStats summary) throws Exception {
@@ -815,23 +823,35 @@
     }
 
     private void expectNetworkStatsUidDetail(NetworkStats detail) throws Exception {
+        expectNetworkStatsUidDetail(detail, new String[0], new NetworkStats(0L, 0));
+    }
+
+    private void expectNetworkStatsUidDetail(
+            NetworkStats detail, String[] tetherIfacePairs, NetworkStats tetherStats)
+            throws Exception {
         expect(mNetManager.getNetworkStatsUidDetail(eq(UID_ALL))).andReturn(detail).atLeastOnce();
+
+        // also include tethering details, since they are folded into UID
+        expect(mConnManager.getTetheredIfacePairs()).andReturn(tetherIfacePairs).atLeastOnce();
+        expect(mNetManager.getNetworkStatsTethering(aryEq(tetherIfacePairs)))
+                .andReturn(tetherStats).atLeastOnce();
     }
 
     private void expectDefaultSettings() throws Exception {
         expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS);
     }
 
-    private void expectSettings(long persistThreshold, long bucketDuration, long maxHistory)
+    private void expectSettings(long persistBytes, long bucketDuration, long deleteAge)
             throws Exception {
         expect(mSettings.getPollInterval()).andReturn(HOUR_IN_MILLIS).anyTimes();
-        expect(mSettings.getPersistThreshold()).andReturn(persistThreshold).anyTimes();
-        expect(mSettings.getNetworkBucketDuration()).andReturn(bucketDuration).anyTimes();
-        expect(mSettings.getNetworkMaxHistory()).andReturn(maxHistory).anyTimes();
-        expect(mSettings.getUidBucketDuration()).andReturn(bucketDuration).anyTimes();
-        expect(mSettings.getUidMaxHistory()).andReturn(maxHistory).anyTimes();
-        expect(mSettings.getTagMaxHistory()).andReturn(maxHistory).anyTimes();
         expect(mSettings.getTimeCacheMaxAge()).andReturn(DAY_IN_MILLIS).anyTimes();
+        expect(mSettings.getGlobalAlertBytes()).andReturn(MB_IN_BYTES).anyTimes();
+        expect(mSettings.getSampleEnabled()).andReturn(true).anyTimes();
+
+        final Config config = new Config(bucketDuration, persistBytes, deleteAge, deleteAge);
+        expect(mSettings.getDevConfig()).andReturn(config).anyTimes();
+        expect(mSettings.getUidConfig()).andReturn(config).anyTimes();
+        expect(mSettings.getUidTagConfig()).andReturn(config).anyTimes();
     }
 
     private void expectCurrentTime() throws Exception {
@@ -843,27 +863,16 @@
     }
 
     private void expectNetworkStatsPoll() throws Exception {
-        expectNetworkStatsPoll(new String[0], new NetworkStats(getElapsedRealtime(), 0));
-    }
-
-    private void expectNetworkStatsPoll(String[] tetherIfacePairs, NetworkStats tetherStats)
-            throws Exception {
         mNetManager.setGlobalAlert(anyLong());
         expectLastCall().anyTimes();
-        expect(mConnManager.getTetheredIfacePairs()).andReturn(tetherIfacePairs).anyTimes();
-        expect(mNetManager.getNetworkStatsTethering(eq(tetherIfacePairs)))
-                .andReturn(tetherStats).anyTimes();
     }
 
     private void assertStatsFilesExist(boolean exist) {
-        final File networkFile = new File(mStatsDir, "netstats.bin");
-        final File uidFile = new File(mStatsDir, "netstats_uid.bin");
+        final File basePath = new File(mStatsDir, "netstats");
         if (exist) {
-            assertTrue(networkFile.exists());
-            assertTrue(uidFile.exists());
+            assertTrue(basePath.list().length > 0);
         } else {
-            assertFalse(networkFile.exists());
-            assertFalse(uidFile.exists());
+            assertTrue(basePath.list().length == 0);
         }
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java
new file mode 100644
index 0000000..7f05f56
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2012 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.net;
+
+import static android.net.NetworkTemplate.buildTemplateMobileAll;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.NetworkIdentity;
+import android.net.NetworkStats;
+import android.net.NetworkTemplate;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.frameworks.servicestests.R;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+/**
+ * Tests for {@link NetworkStatsCollection}.
+ */
+@MediumTest
+public class NetworkStatsCollectionTest extends AndroidTestCase {
+    
+    private static final String TEST_FILE = "test.bin";
+    private static final String TEST_IMSI = "310260000000000";
+
+    public void testReadLegacyNetwork() throws Exception {
+        final File testFile = new File(getContext().getFilesDir(), TEST_FILE);
+        stageFile(R.raw.netstats_v1, testFile);
+
+        final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+        collection.readLegacyNetwork(testFile);
+        
+        // verify that history read correctly
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                636014522L, 709291L, 88037144L, 518820L);
+
+        // now export into a unified format
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        collection.write(new DataOutputStream(bos));
+
+        // clear structure completely
+        collection.reset();
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                0L, 0L, 0L, 0L);
+
+        // and read back into structure, verifying that totals are same
+        collection.read(new ByteArrayInputStream(bos.toByteArray()));
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                636014522L, 709291L, 88037144L, 518820L);
+    }
+
+    public void testReadLegacyUid() throws Exception {
+        final File testFile = new File(getContext().getFilesDir(), TEST_FILE);
+        stageFile(R.raw.netstats_uid_v4, testFile);
+
+        final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+        collection.readLegacyUid(testFile, false);
+
+        // verify that history read correctly
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                637073904L, 711398L, 88342093L, 521006L);
+
+        // now export into a unified format
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        collection.write(new DataOutputStream(bos));
+
+        // clear structure completely
+        collection.reset();
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                0L, 0L, 0L, 0L);
+
+        // and read back into structure, verifying that totals are same
+        collection.read(new ByteArrayInputStream(bos.toByteArray()));
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                637073904L, 711398L, 88342093L, 521006L);
+    }
+
+    public void testReadLegacyUidTags() throws Exception {
+        final File testFile = new File(getContext().getFilesDir(), TEST_FILE);
+        stageFile(R.raw.netstats_uid_v4, testFile);
+
+        final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+        collection.readLegacyUid(testFile, true);
+
+        // verify that history read correctly
+        assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI),
+                77017831L, 100995L, 35436758L, 92344L);
+
+        // now export into a unified format
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        collection.write(new DataOutputStream(bos));
+
+        // clear structure completely
+        collection.reset();
+        assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI),
+                0L, 0L, 0L, 0L);
+
+        // and read back into structure, verifying that totals are same
+        collection.read(new ByteArrayInputStream(bos.toByteArray()));
+        assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI),
+                77017831L, 100995L, 35436758L, 92344L);
+    }
+
+    /**
+     * Copy a {@link Resources#openRawResource(int)} into {@link File} for
+     * testing purposes.
+     */
+    private void stageFile(int rawId, File file) throws Exception {
+        new File(file.getParent()).mkdirs();
+        InputStream in = null;
+        OutputStream out = null;
+        try {
+            in = getContext().getResources().openRawResource(rawId);
+            out = new FileOutputStream(file);
+            Streams.copy(in, out);
+        } finally {
+            IoUtils.closeQuietly(in);
+            IoUtils.closeQuietly(out);
+        }
+    }
+
+    public static NetworkIdentitySet buildWifiIdent() {
+        final NetworkIdentitySet set = new NetworkIdentitySet();
+        set.add(new NetworkIdentity(ConnectivityManager.TYPE_WIFI, 0, null, false));
+        return set;
+    }
+
+    private static void assertSummaryTotal(NetworkStatsCollection collection,
+            NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+        final NetworkStats.Entry entry = collection.getSummary(
+                template, Long.MIN_VALUE, Long.MAX_VALUE).getTotal(null);
+        assertEntry(entry, rxBytes, rxPackets, txBytes, txPackets);
+    }
+
+    private static void assertSummaryTotalIncludingTags(NetworkStatsCollection collection,
+            NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+        final NetworkStats.Entry entry = collection.getSummary(
+                template, Long.MIN_VALUE, Long.MAX_VALUE).getTotalIncludingTags(null);
+        assertEntry(entry, rxBytes, rxPackets, txBytes, txPackets);
+    }
+
+    private static void assertEntry(
+            NetworkStats.Entry entry, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+        assertEquals("unexpected txPackets", txPackets, entry.txPackets);
+    }
+}
diff --git a/tests/ActivityTests/res/anim/slow_enter.xml b/tests/ActivityTests/res/anim/slow_enter.xml
new file mode 100644
index 0000000..0309643
--- /dev/null
+++ b/tests/ActivityTests/res/anim/slow_enter.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2011, 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.
+*/
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+        android:shareInterpolator="false" >
+    <scale android:fromXScale="0.9" android:toXScale="1.5"
+           android:fromYScale="0.9" android:toYScale="1.5"
+           android:pivotX="50%" android:pivotY="50%"
+           android:interpolator="@interpolator/slow_enter"
+           android:duration="40000" />
+    <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+            android:interpolator="@android:interpolator/decelerate_cubic"
+            android:duration="1000" />
+</set>
diff --git a/tests/ActivityTests/res/anim/slow_exit.xml b/tests/ActivityTests/res/anim/slow_exit.xml
new file mode 100644
index 0000000..6cd3114
--- /dev/null
+++ b/tests/ActivityTests/res/anim/slow_exit.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2011, 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.
+*/
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+        android:shareInterpolator="false" >
+    <scale android:fromXScale="1.0" android:toXScale="0.9"
+            android:fromYScale="1.0" android:toYScale="0.9"
+            android:pivotX="50%" android:pivotY="50%"
+            android:interpolator="@android:interpolator/decelerate_quint"
+            android:duration="300" />
+    <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
+            android:interpolator="@android:interpolator/decelerate_cubic"
+            android:duration="300"/>
+</set>
diff --git a/tests/ActivityTests/res/interpolator/slow_enter.xml b/tests/ActivityTests/res/interpolator/slow_enter.xml
new file mode 100644
index 0000000..ddab1aa
--- /dev/null
+++ b/tests/ActivityTests/res/interpolator/slow_enter.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2011, 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.
+*/
+-->
+
+<cycleInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:cycles="10" />
diff --git a/tests/ActivityTests/res/values/themes.xml b/tests/ActivityTests/res/values/themes.xml
new file mode 100644
index 0000000..67f5938
--- /dev/null
+++ b/tests/ActivityTests/res/values/themes.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<resources>
+    <style name="SlowDialog" parent="@android:style/Theme.Holo.Dialog">
+        <item name="android:windowAnimationStyle">@style/SlowDialog</item>
+    </style>
+    <style name="SlowDialog">
+        <item name="android:windowEnterAnimation">@anim/slow_enter</item>
+        <item name="android:windowExitAnimation">@anim/slow_exit</item>
+    </style>
+</resources>
diff --git a/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java b/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java
index 583c13c..ae42e29 100644
--- a/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java
+++ b/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java
@@ -22,6 +22,7 @@
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.ActivityThread;
+import android.app.AlertDialog;
 import android.app.Application;
 import android.content.ActivityNotFoundException;
 import android.os.Bundle;
@@ -35,6 +36,8 @@
 import android.widget.TextView;
 import android.widget.ScrollView;
 import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
 import android.view.View;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
@@ -101,6 +104,20 @@
     }
 
     @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        menu.add("Animate!").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+            @Override public boolean onMenuItemClick(MenuItem item) {
+                AlertDialog.Builder builder = new AlertDialog.Builder(ActivityTestMain.this,
+                        R.style.SlowDialog);
+                builder.setTitle("This is a title");
+                builder.show();
+                return true;
+            }
+        });
+        return true;
+    }
+
+    @Override
     protected void onStart() {
         super.onStart();
         buildUi();
diff --git a/tests/TileBenchmark/res/values/strings.xml b/tests/TileBenchmark/res/values/strings.xml
index 5af52dc..6c7055b 100644
--- a/tests/TileBenchmark/res/values/strings.xml
+++ b/tests/TileBenchmark/res/values/strings.xml
@@ -49,8 +49,9 @@
     <!-- Drop down menu entry - automatically scroll to the end of the page
     with scrollBy() [CHAR LIMIT=15] -->
     <string name="movement_auto_scroll">Auto-scroll</string>
-    <!-- Drop down menu entry -  [CHAR LIMIT=15] -->
-    <string name="movement_auto_fling">Auto-fling</string>
+    <!-- Drop down menu entry - automatically record for a set time before
+    stopping [CHAR LIMIT=15] -->
+    <string name="movement_timed">Timed</string>
     <!-- Drop down menu entry - manually navigate the page(s), hit 'capture'
     button [CHAR LIMIT=15] -->
     <string name="movement_manual">Manual</string>
@@ -67,14 +68,21 @@
     <!-- 75th percentile - 75% of frames fall below this value [CHAR LIMIT=12]
     -->
     <string name="percentile_75">75%ile</string>
+    <!-- standard deviation [CHAR LIMIT=12] -->
+    <string name="std_dev">StdDev</string>
+    <!-- mean [CHAR LIMIT=12] -->
+    <string name="mean">mean</string>
+
+
+
     <!-- Frame rate [CHAR LIMIT=15] -->
     <string name="frames_per_second">Frames/sec</string>
     <!-- Portion of viewport covered by good tiles [CHAR LIMIT=15] -->
     <string name="viewport_coverage">Coverage</string>
     <!-- Milliseconds taken to inval, and re-render the page [CHAR LIMIT=15] -->
     <string name="render_millis">RenderMillis</string>
-    <!-- Number of rendering stalls while running the test [CHAR LIMIT=15] -->
-    <string name="render_stalls">Stalls</string>
+    <!-- Animation Framerate [CHAR LIMIT=15] -->
+    <string name="animation_framerate">AnimFramerate</string>
     <!-- Format string for stat value overlay [CHAR LIMIT=15] -->
     <string name="format_stat">%4.4f</string>
 
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/PerformanceTest.java b/tests/TileBenchmark/src/com/test/tilebenchmark/PerformanceTest.java
index cc39b75..6356cc1 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/PerformanceTest.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/PerformanceTest.java
@@ -27,17 +27,57 @@
 import android.os.Environment;
 import android.test.ActivityInstrumentationTestCase2;
 import android.util.Log;
+import android.webkit.WebSettings;
+import android.widget.Spinner;
 
 public class PerformanceTest extends
         ActivityInstrumentationTestCase2<ProfileActivity> {
 
+    public static class AnimStat {
+        double aggVal = 0;
+        double aggSqrVal = 0;
+        double count = 0;
+    }
+
     private class StatAggregator extends PlaybackGraphs {
         private HashMap<String, Double> mDataMap = new HashMap<String, Double>();
+        private HashMap<String, AnimStat> mAnimDataMap = new HashMap<String, AnimStat>();
         private int mCount = 0;
 
+
         public void aggregate() {
+            boolean inAnimTests = mAnimTests != null;
+            Resources resources = mWeb.getResources();
+            String animFramerateString = resources.getString(R.string.animation_framerate);
+            for (Map.Entry<String, Double> e : mSingleStats.entrySet()) {
+                String name = e.getKey();
+                if (inAnimTests) {
+                    if (name.equals(animFramerateString)) {
+                        // in animation testing phase, record animation framerate and aggregate
+                        // stats, differentiating on values of mAnimTestNr and mDoubleBuffering
+                        String fullName = ANIM_TEST_NAMES[mAnimTestNr] + " " + name;
+                        fullName += mDoubleBuffering ? " tiled" : " webkit";
+
+                        if (!mAnimDataMap.containsKey(fullName)) {
+                            mAnimDataMap.put(fullName, new AnimStat());
+                        }
+                        AnimStat statVals = mAnimDataMap.get(fullName);
+                        statVals.aggVal += e.getValue();
+                        statVals.aggSqrVal += e.getValue() * e.getValue();
+                        statVals.count += 1;
+                    }
+                } else {
+                    double aggVal = mDataMap.containsKey(name)
+                            ? mDataMap.get(name) : 0;
+                    mDataMap.put(name, aggVal + e.getValue());
+                }
+            }
+
+            if (inAnimTests) {
+                return;
+            }
+
             mCount++;
-            Resources resources = mView.getResources();
             for (int metricIndex = 0; metricIndex < Metrics.length; metricIndex++) {
                 for (int statIndex = 0; statIndex < Stats.length; statIndex++) {
                     String metricLabel = resources.getString(
@@ -53,34 +93,47 @@
                     mDataMap.put(label, aggVal);
                 }
             }
-            for (Map.Entry<String, Double> e : mSingleStats.entrySet()) {
-                double aggVal = mDataMap.containsKey(e.getKey())
-                        ? mDataMap.get(e.getKey()) : 0;
-                mDataMap.put(e.getKey(), aggVal + e.getValue());
-            }
+
         }
 
+        // build the final bundle of results
         public Bundle getBundle() {
             Bundle b = new Bundle();
-            int count = 0 == mCount ? Integer.MAX_VALUE : mCount;
+            int count = (0 == mCount) ? Integer.MAX_VALUE : mCount;
             for (Map.Entry<String, Double> e : mDataMap.entrySet()) {
                 b.putDouble(e.getKey(), e.getValue() / count);
             }
+
+            for (Map.Entry<String, AnimStat> e : mAnimDataMap.entrySet()) {
+                String statName = e.getKey();
+                AnimStat statVals = e.getValue();
+
+                double avg = statVals.aggVal/statVals.count;
+                double stdDev = Math.sqrt((statVals.aggSqrVal / statVals.count) - avg * avg);
+
+                b.putDouble(statName, avg);
+                b.putDouble(statName + " STD DEV", stdDev);
+            }
+
             return b;
         }
     }
 
     ProfileActivity mActivity;
-    ProfiledWebView mView;
-    StatAggregator mStats = new StatAggregator();
+    ProfiledWebView mWeb;
+    Spinner mMovementSpinner;
+    StatAggregator mStats;
 
     private static final String LOGTAG = "PerformanceTest";
     private static final String TEST_LOCATION = "webkit/page_cycler";
     private static final String URL_PREFIX = "file://";
     private static final String URL_POSTFIX = "/index.html?skip=true";
     private static final int MAX_ITERATIONS = 4;
-    private static final String TEST_DIRS[] = {
-        "alexa25_2011"//, "alexa_us", "android", "dom", "intl2", "moz", "moz2"
+    private static final String SCROLL_TEST_DIRS[] = {
+        "alexa25_2011"
+    };
+    private static final String ANIM_TEST_DIRS[] = {
+        "dhtml"
     };
 
     public PerformanceTest() {
@@ -91,7 +144,22 @@
     protected void setUp() throws Exception {
         super.setUp();
         mActivity = getActivity();
-        mView = (ProfiledWebView) mActivity.findViewById(R.id.web);
+        mWeb = (ProfiledWebView) mActivity.findViewById(R.id.web);
+        mMovementSpinner = (Spinner) mActivity.findViewById(R.id.movement);
+        mStats = new StatAggregator();
+
+        // use mStats as a condition variable between the UI thread and
+        // this(the testing) thread
+        mActivity.setCallback(new ProfileCallback() {
+            @Override
+            public void profileCallback(RunData data) {
+                mStats.setData(data);
+                synchronized (mStats) {
+                    mStats.notify();
+                }
+            }
+        });
+
     }
 
     private boolean loadUrl(final String url) {
@@ -100,12 +168,13 @@
             mActivity.runOnUiThread(new Runnable() {
                 @Override
                 public void run() {
-                    mView.loadUrl(url);
+                    mWeb.loadUrl(url);
                 }
             });
             synchronized (mStats) {
                 mStats.wait();
             }
+
             mStats.aggregate();
         } catch (InterruptedException e) {
             e.printStackTrace();
@@ -114,15 +183,30 @@
         return true;
     }
 
-    private boolean runIteration() {
+    private boolean validTest(String nextTest) {
+        // if testing animations, test must be in mAnimTests
+        if (mAnimTests == null)
+            return true;
+
+        for (String test : mAnimTests) {
+            if (test.equals(nextTest)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean runIteration(String[] testDirs) {
         File sdFile = Environment.getExternalStorageDirectory();
-        for (String testDirName : TEST_DIRS) {
+        for (String testDirName : testDirs) {
             File testDir = new File(sdFile, TEST_LOCATION + "/" + testDirName);
             Log.d(LOGTAG, "Testing dir: '" + testDir.getAbsolutePath()
                     + "', exists=" + testDir.exists());
+
             for (File siteDir : testDir.listFiles()) {
-                if (!siteDir.isDirectory())
+                if (!siteDir.isDirectory() || !validTest(siteDir.getName())) {
                     continue;
+                }
 
                 if (!loadUrl(URL_PREFIX + siteDir.getAbsolutePath()
                         + URL_POSTFIX)) {
@@ -133,7 +217,44 @@
         return true;
     }
 
-    public void testMetrics() {
+    private boolean  runTestDirs(String[] testDirs) {
+        for (int i = 0; i < MAX_ITERATIONS; i++)
+            if (!runIteration(testDirs)) {
+                return false;
+            }
+        return true;
+    }
+
+    private void pushDoubleBuffering() {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            public void run() {
+                mWeb.setDoubleBuffering(mDoubleBuffering);
+            }
+        });
+    }
+
+    private void setScrollingTestingMode(final boolean scrolled) {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            public void run() {
+                mMovementSpinner.setSelection(scrolled ? 0 : 2);
+            }
+        });
+    }
+
+
+    private String[] mAnimTests = null;
+    private int mAnimTestNr = -1;
+    private boolean mDoubleBuffering = true;
+    private static final String[] ANIM_TEST_NAMES = {
+        "slow", "fast"
+    };
+    private static final String[][] ANIM_TESTS = {
+        {"scrolling", "replaceimages", "layers5", "layers1"},
+        {"slidingballs", "meter", "slidein", "fadespacing", "colorfade",
+                "mozilla", "movingtext", "diagball", "zoom", "imageslide"},
+    };
+
+    private boolean checkMedia() {
         String state = Environment.getExternalStorageState();
 
         if (!Environment.MEDIA_MOUNTED.equals(state)
@@ -141,27 +262,43 @@
             Log.d(LOGTAG, "ARG Can't access sd card!");
             // Can't read the SD card, fail and die!
             getInstrumentation().sendStatus(1, null);
-            return;
+            return false;
         }
+        return true;
+    }
 
-        // use mGraphs as a condition variable between the UI thread and
-        // this(the testing) thread
-        mActivity.setCallback(new ProfileCallback() {
-            @Override
-            public void profileCallback(RunData data) {
-                Log.d(LOGTAG, "test completion callback");
-                mStats.setData(data);
-                synchronized (mStats) {
-                    mStats.notify();
+    public void testMetrics() {
+        setScrollingTestingMode(true);
+        if (checkMedia() && runTestDirs(SCROLL_TEST_DIRS)) {
+            getInstrumentation().sendStatus(0, mStats.getBundle());
+        } else {
+            getInstrumentation().sendStatus(1, null);
+        }
+    }
+
+    private boolean runAnimationTests() {
+        for (int doubleBuffer = 0; doubleBuffer <= 1; doubleBuffer++) {
+            mDoubleBuffering = doubleBuffer == 1;
+            pushDoubleBuffering();
+            for (mAnimTestNr = 0; mAnimTestNr < ANIM_TESTS.length; mAnimTestNr++) {
+                mAnimTests = ANIM_TESTS[mAnimTestNr];
+                if (!runTestDirs(ANIM_TEST_DIRS)) {
+                    return false;
                 }
             }
-        });
+        }
+        return true;
+    }
 
-        for (int i = 0; i < MAX_ITERATIONS; i++)
-            if (!runIteration()) {
-                getInstrumentation().sendStatus(1, null);
-                return;
-            }
-        getInstrumentation().sendStatus(0, mStats.getBundle());
+    public void testAnimations() {
+        // instead of autoscrolling, load each page until either an timer fires,
+        // or the animation signals complete via javascript
+        setScrollingTestingMode(false);
+
+        if (checkMedia() && runAnimationTests()) {
+            getInstrumentation().sendStatus(0, mStats.getBundle());
+        } else {
+            getInstrumentation().sendStatus(1, null);
+        }
     }
 }
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java
index 9ea90f8..a3ae9be 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java
@@ -80,7 +80,7 @@
                     for (int tileID = 1; tileID < frame.length; tileID++) {
                         TileData data = frame[tileID];
                         double coverage = viewportCoverage(frame[0], data);
-                        total += coverage * (data.isReady ? 1 : 0);
+                        total += coverage * (data.isReady ? 100 : 0);
                         totalCount += coverage;
                     }
                     if (totalCount == 0) {
@@ -91,7 +91,7 @@
 
                 @Override
                 public double getMax() {
-                    return 1;
+                    return 100;
                 }
 
                 @Override
@@ -108,6 +108,9 @@
     }
 
     public static double getPercentile(double sortedValues[], double ratioAbove) {
+        if (sortedValues.length == 0)
+            return -1;
+
         double index = ratioAbove * (sortedValues.length - 1);
         int intIndex = (int) Math.floor(index);
         if (index == intIndex) {
@@ -118,6 +121,31 @@
                 + sortedValues[intIndex + 1] * (alpha);
     }
 
+    public static double getMean(double sortedValues[]) {
+        if (sortedValues.length == 0)
+            return -1;
+
+        double agg = 0;
+        for (double val : sortedValues) {
+            agg += val;
+        }
+        return agg / sortedValues.length;
+    }
+
+    public static double getStdDev(double sortedValues[]) {
+        if (sortedValues.length == 0)
+            return -1;
+
+        double agg = 0;
+        double sqrAgg = 0;
+        for (double val : sortedValues) {
+            agg += val;
+            sqrAgg += val*val;
+        }
+        double mean = agg / sortedValues.length;
+        return Math.sqrt((sqrAgg / sortedValues.length) - (mean * mean));
+    }
+
     protected static StatGen[] Stats = new StatGen[] {
             new StatGen() {
                 @Override
@@ -149,6 +177,26 @@
                 public int getLabelId() {
                     return R.string.percentile_75;
                 }
+            }, new StatGen() {
+                @Override
+                public double getValue(double[] sortedValues) {
+                    return getStdDev(sortedValues);
+                }
+
+                @Override
+                public int getLabelId() {
+                    return R.string.std_dev;
+                }
+            }, new StatGen() {
+                @Override
+                public double getValue(double[] sortedValues) {
+                    return getMean(sortedValues);
+                }
+
+                @Override
+                public int getLabelId() {
+                    return R.string.mean;
+                }
             },
     };
 
@@ -159,40 +207,47 @@
     }
 
     private ArrayList<ShapeDrawable> mShapes = new ArrayList<ShapeDrawable>();
-    protected double[][] mStats = new double[Metrics.length][Stats.length];
+    protected final double[][] mStats = new double[Metrics.length][Stats.length];
     protected HashMap<String, Double> mSingleStats;
 
+    private void gatherFrameMetric(int metricIndex, double metricValues[], RunData data) {
+        // create graph out of rectangles, one per frame
+        int lastBar = 0;
+        for (int frameIndex = 0; frameIndex < data.frames.length; frameIndex++) {
+            TileData frame[] = data.frames[frameIndex];
+            int newBar = (frame[0].top + frame[0].bottom) / 2;
+
+            MetricGen s = Metrics[metricIndex];
+            double absoluteValue = s.getValue(frame);
+            double relativeValue = absoluteValue / s.getMax();
+            relativeValue = Math.min(1,relativeValue);
+            relativeValue = Math.max(0,relativeValue);
+            int rightPos = (int) (-BAR_WIDTH * metricIndex);
+            int leftPos = (int) (-BAR_WIDTH * (metricIndex + relativeValue));
+
+            ShapeDrawable graphBar = new ShapeDrawable();
+            graphBar.getPaint().setColor(Color.BLUE);
+            graphBar.setBounds(leftPos, lastBar, rightPos, newBar);
+
+            mShapes.add(graphBar);
+            metricValues[frameIndex] = absoluteValue;
+            lastBar = newBar;
+        }
+    }
+
     public void setData(RunData data) {
         mShapes.clear();
         double metricValues[] = new double[data.frames.length];
 
+        mSingleStats = data.singleStats;
+
         if (data.frames.length == 0) {
             return;
         }
 
         for (int metricIndex = 0; metricIndex < Metrics.length; metricIndex++) {
-            // create graph out of rectangles, one per frame
-            int lastBar = 0;
-            for (int frameIndex = 0; frameIndex < data.frames.length; frameIndex++) {
-                TileData frame[] = data.frames[frameIndex];
-                int newBar = (frame[0].top + frame[0].bottom) / 2;
-
-                MetricGen s = Metrics[metricIndex];
-                double absoluteValue = s.getValue(frame);
-                double relativeValue = absoluteValue / s.getMax();
-                relativeValue = Math.min(1,relativeValue);
-                relativeValue = Math.max(0,relativeValue);
-                int rightPos = (int) (-BAR_WIDTH * metricIndex);
-                int leftPos = (int) (-BAR_WIDTH * (metricIndex + relativeValue));
-
-                ShapeDrawable graphBar = new ShapeDrawable();
-                graphBar.getPaint().setColor(Color.BLUE);
-                graphBar.setBounds(leftPos, lastBar, rightPos, newBar);
-
-                mShapes.add(graphBar);
-                metricValues[frameIndex] = absoluteValue;
-                lastBar = newBar;
-            }
+            // calculate metric based on list of frames
+            gatherFrameMetric(metricIndex, metricValues, data);
 
             // store aggregate statistics per metric (median, and similar)
             Arrays.sort(metricValues);
@@ -200,8 +255,6 @@
                 mStats[metricIndex][statIndex] =
                         Stats[statIndex].getValue(metricValues);
             }
-
-            mSingleStats = data.singleStats;
         }
     }
 
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java
index d38d006..2e77157 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java
@@ -22,11 +22,12 @@
 import android.graphics.Bitmap;
 import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.CountDownTimer;
+import android.util.Log;
 import android.util.Pair;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
-import android.webkit.WebSettings;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import android.widget.AdapterView;
@@ -49,6 +50,8 @@
  */
 public class ProfileActivity extends Activity {
 
+    private static final int TIMED_RECORD_MILLIS = 2000;
+
     public interface ProfileCallback {
         public void profileCallback(RunData data);
     }
@@ -65,6 +68,7 @@
 
     LoggingWebViewClient mLoggingWebViewClient = new LoggingWebViewClient();
     AutoLoggingWebViewClient mAutoLoggingWebViewClient = new AutoLoggingWebViewClient();
+    TimedLoggingWebViewClient mTimedLoggingWebViewClient = new TimedLoggingWebViewClient();
 
     private enum TestingState {
         NOT_TESTING,
@@ -93,18 +97,18 @@
         public void onItemSelected(AdapterView<?> parent, View view,
                 int position, long id) {
             String movementStr = parent.getItemAtPosition(position).toString();
-            if (movementStr == getResources().getString(
-                    R.string.movement_auto_scroll)
-                    || movementStr == getResources().getString(
-                            R.string.movement_auto_fling)) {
+            if (movementStr == getResources().getString(R.string.movement_auto_scroll)) {
                 mWeb.setWebViewClient(mAutoLoggingWebViewClient);
                 mCaptureButton.setEnabled(false);
                 mVelocitySpinner.setEnabled(true);
-            } else if (movementStr == getResources().getString(
-                    R.string.movement_manual)) {
+            } else if (movementStr == getResources().getString(R.string.movement_manual)) {
                 mWeb.setWebViewClient(mLoggingWebViewClient);
                 mCaptureButton.setEnabled(true);
                 mVelocitySpinner.setEnabled(false);
+            } else if (movementStr == getResources().getString(R.string.movement_timed)) {
+                mWeb.setWebViewClient(mTimedLoggingWebViewClient);
+                mCaptureButton.setEnabled(false);
+                mVelocitySpinner.setEnabled(false);
             }
         }
 
@@ -124,15 +128,19 @@
             super.onPageStarted(view, url, favicon);
             mUrl.setText(url);
         }
-    }
-
-    private class AutoLoggingWebViewClient extends LoggingWebViewClient {
 
         @Override
         public void onPageFinished(WebView view, String url) {
             super.onPageFinished(view, url);
             view.requestFocus();
+            ((ProfiledWebView)view).onPageFinished();
+        }
+    }
 
+    private class AutoLoggingWebViewClient extends LoggingWebViewClient {
+        @Override
+        public void onPageFinished(WebView view, String url) {
+            super.onPageFinished(view, url);
             startViewProfiling(true);
         }
 
@@ -143,6 +151,32 @@
         }
     }
 
+    private class TimedLoggingWebViewClient extends LoggingWebViewClient {
+        @Override
+        public void onPageFinished(WebView view, String url) {
+            super.onPageFinished(view, url);
+            startViewProfiling(false);
+
+            // after a fixed time after page finished, stop testing
+            new CountDownTimer(TIMED_RECORD_MILLIS, TIMED_RECORD_MILLIS) {
+                @Override
+                public void onTick(long millisUntilFinished) {
+                }
+
+                @Override
+                public void onFinish() {
+                    mWeb.stopScrollTest();
+                }
+            }.start();
+        }
+
+        @Override
+        public void onPageStarted(WebView view, String url, Bitmap favicon) {
+            super.onPageStarted(view, url, favicon);
+            setTestingState(TestingState.PRE_TESTING);
+        }
+    }
+
     private class StoreFileTask extends
             AsyncTask<Pair<String, RunData>, Void, Void> {
 
@@ -178,11 +212,13 @@
                 mMovementSpinner.setEnabled(false);
                 break;
             case START_TESTING:
+                mCaptureButton.setChecked(true);
                 mUrl.setBackgroundResource(R.color.background_start_testing);
                 mInspectButton.setEnabled(false);
                 mMovementSpinner.setEnabled(false);
                 break;
             case STOP_TESTING:
+                mCaptureButton.setChecked(false);
                 mUrl.setBackgroundResource(R.color.background_stop_testing);
                 break;
             case SAVED_TESTING:
@@ -195,7 +231,6 @@
     /** auto - automatically scroll. */
     private void startViewProfiling(boolean auto) {
         // toggle capture button to indicate capture state to user
-        mCaptureButton.setChecked(true);
         mWeb.startScrollTest(mCallback, auto);
         setTestingState(TestingState.START_TESTING);
     }
@@ -217,7 +252,7 @@
             public void profileCallback(RunData data) {
                 new StoreFileTask().execute(new Pair<String, RunData>(
                         TEMP_FILENAME, data));
-                mCaptureButton.setChecked(false);
+                Log.d("ProfileActivity", "stored " + data.frames.length + " frames in file");
                 setTestingState(TestingState.STOP_TESTING);
             }
         });
@@ -245,8 +280,8 @@
         // Movement spinner
         String content[] = {
                 getResources().getString(R.string.movement_auto_scroll),
-                getResources().getString(R.string.movement_auto_fling),
-                getResources().getString(R.string.movement_manual)
+                getResources().getString(R.string.movement_manual),
+                getResources().getString(R.string.movement_timed)
         };
         adapter = new ArrayAdapter<CharSequence>(this,
                 android.R.layout.simple_spinner_item, content);
@@ -270,13 +305,7 @@
         });
 
         // Custom profiling WebView
-        WebSettings settings = mWeb.getSettings();
-        settings.setJavaScriptEnabled(true);
-        settings.setSupportZoom(true);
-        settings.setEnableSmoothTransition(true);
-        settings.setBuiltInZoomControls(true);
-        settings.setLoadWithOverviewMode(true);
-        settings.setProperty("use_minimal_memory", "false"); // prefetch tiles, as browser does
+        mWeb.init(this);
         mWeb.setWebViewClient(new LoggingWebViewClient());
 
         // URL text entry
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java
index 83f1668..a706f78 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java
@@ -20,21 +20,28 @@
 import android.os.CountDownTimer;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.webkit.WebSettings;
 import android.webkit.WebView;
+import android.widget.Toast;
+
+import java.util.ArrayList;
 
 import com.test.tilebenchmark.ProfileActivity.ProfileCallback;
 import com.test.tilebenchmark.RunData.TileData;
 
 public class ProfiledWebView extends WebView {
+    private static final String LOGTAG = "ProfiledWebView";
+
     private int mSpeed;
 
     private boolean mIsTesting = false;
     private boolean mIsScrolling = false;
     private ProfileCallback mCallback;
     private long mContentInvalMillis;
-    private boolean mHadToBeForced = false;
     private static final int LOAD_STALL_MILLIS = 2000; // nr of millis after load,
                                                        // before test is forced
+    private double mLoadTime;
+    private double mAnimationTime;
 
     public ProfiledWebView(Context context) {
         super(context);
@@ -53,6 +60,39 @@
         super(context, attrs, defStyle, privateBrowsing);
     }
 
+    private class JavaScriptInterface {
+        Context mContext;
+
+        /** Instantiate the interface and set the context */
+        JavaScriptInterface(Context c) {
+            mContext = c;
+        }
+
+        /** Show a toast from the web page */
+        public void animationComplete() {
+            Toast.makeText(mContext, "Animation complete!", Toast.LENGTH_SHORT).show();
+            //Log.d(LOGTAG, "anim complete");
+            mAnimationTime = System.currentTimeMillis();
+        }
+    }
+
+    public void init(Context c) {
+        WebSettings settings = getSettings();
+        settings.setJavaScriptEnabled(true);
+        settings.setSupportZoom(true);
+        settings.setEnableSmoothTransition(true);
+        settings.setBuiltInZoomControls(true);
+        settings.setLoadWithOverviewMode(true);
+        settings.setProperty("use_minimal_memory", "false"); // prefetch tiles, as browser does
+        addJavascriptInterface(new JavaScriptInterface(c), "Android");
+        mAnimationTime = 0;
+        mLoadTime = 0;
+    }
+
+    public void onPageFinished() {
+        mLoadTime = System.currentTimeMillis();
+    }
+
     @Override
     protected void onDraw(android.graphics.Canvas canvas) {
         if (mIsTesting && mIsScrolling) {
@@ -72,9 +112,12 @@
      * scrolling, invalidate all content and redraw it, measuring time taken.
      */
     public void startScrollTest(ProfileCallback callback, boolean autoScrolling) {
-        mIsScrolling = autoScrolling;
         mCallback = callback;
         mIsTesting = false;
+        mIsScrolling = false;
+        WebSettings settings = getSettings();
+        settings.setProperty("tree_updates", "0");
+
 
         if (autoScrolling) {
             // after a while, force it to start even if the pages haven't swapped
@@ -86,13 +129,18 @@
                 @Override
                 public void onFinish() {
                     // invalidate all content, and kick off redraw
+                    Log.d("ProfiledWebView",
+                            "kicking off test with callback registration, and tile discard...");
                     registerPageSwapCallback();
                     discardAllTextures();
                     invalidate();
-
+                    mIsScrolling = true;
                     mContentInvalMillis = System.currentTimeMillis();
                 }
             }.start();
+        } else {
+            mIsTesting = true;
+            tileProfilingStart();
         }
     }
 
@@ -102,13 +150,35 @@
      */
     @Override
     protected void pageSwapCallback(boolean startAnim) {
-        mContentInvalMillis = System.currentTimeMillis() - mContentInvalMillis;
-        super.pageSwapCallback(startAnim);
-        Log.d("ProfiledWebView", "REDRAW TOOK " + mContentInvalMillis
-                + "millis");
-        mIsTesting = true;
-        invalidate(); // ensure a redraw so that auto-scrolling can occur
-        tileProfilingStart();
+        if (!mIsTesting && mIsScrolling) {
+            // kick off testing
+            mContentInvalMillis = System.currentTimeMillis() - mContentInvalMillis;
+            super.pageSwapCallback(startAnim);
+            Log.d("ProfiledWebView", "REDRAW TOOK " + mContentInvalMillis + "millis");
+            mIsTesting = true;
+            invalidate(); // ensure a redraw so that auto-scrolling can occur
+            tileProfilingStart();
+        }
+    }
+
+    private double animFramerate() {
+        WebSettings settings = getSettings();
+        String updatesString = settings.getProperty("tree_updates");
+        int updates = (updatesString == null) ? -1 : Integer.parseInt(updatesString);
+
+        double animationTime;
+        if (mAnimationTime == 0) {
+            animationTime = System.currentTimeMillis() - mLoadTime;
+        } else {
+            animationTime = mAnimationTime - mLoadTime;
+        }
+
+        return updates * 1000 / animationTime;
+    }
+
+    public void setDoubleBuffering(boolean useDoubleBuffering) {
+        WebSettings settings = getSettings();
+        settings.setProperty("use_double_buffering", useDoubleBuffering ? "true" : "false");
     }
 
     /*
@@ -127,11 +197,12 @@
         // record the time spent (before scrolling) rendering the page
         data.singleStats.put(getResources().getString(R.string.render_millis),
                 (double)mContentInvalMillis);
-        // record if the page render timed out
-        Log.d("ProfiledWebView", "hadtobeforced = " + mHadToBeForced);
-        data.singleStats.put(getResources().getString(R.string.render_stalls),
-                             mHadToBeForced ? 1.0 : 0.0);
-        mHadToBeForced = false;
+
+        // record framerate
+        double framerate = animFramerate();
+        Log.d(LOGTAG, "anim framerate was "+framerate);
+        data.singleStats.put(getResources().getString(R.string.animation_framerate),
+                framerate);
 
         for (int frame = 0; frame < data.frames.length; frame++) {
             data.frames[frame] = new TileData[
@@ -159,6 +230,8 @@
 
     @Override
     public void loadUrl(String url) {
+        mAnimationTime = 0;
+        mLoadTime = 0;
         if (!url.startsWith("http://") && !url.startsWith("file://")) {
             url = "http://" + url;
         }