Move network stats to FileRotator pattern.

Split existing network stats into two separate classes: a recorder
which generates historical data based on periodic counter snapshots,
and a collection of historical data with persistance logic.

Recorder keeps a pending history in memory until outstanding data
crosses a specific threshold.  Persisting is handled through a given
FileRotator.  This pattern significantly reduces disk churn and
memory overhead.  Separate UID data from UID tag data, enabling a
shorter rotation cycle.  Migrate existing stats into new structure.

Remove "xt" stats until iptables hooks are ready.  Avoid consuming
Entry values when recording into NetworkStatsHistory.  Assign
operation counts to default route interface.

Introduce "Rewriter" interface in FileRotator with methods to enable
rewriteAll().  Introduce IndentingPrintWriter to handle indenting in
dump() methods.

Bug: 5386531
Change-Id: Ibe086230a17999a197206ca62d45f266225fdff1
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/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);
+    }
+}