Persist UID stats, lazy loading, resize buckets.

Persisting detailed UID stats in separate "netstats_detail.bin" file
to enable different schedules for summary and detail polling.  Only
load detailed UID history on demand, since it's not needed during
boot.  Add test to verify UID stats are persisted across simulated
reboot.

Move external settings into well-named interface, which is still
backed by Settings.Secure.  During periodic poll events, resize any
history to match current bucket duration setting.  Test to verify.

Change-Id: I6366f3583a591f8ba859b0e5987daf8cafa4e95a
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 6ab7738..1025b20 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -3800,13 +3800,13 @@
         /** {@hide} */
         public static final String NETSTATS_PERSIST_THRESHOLD = "netstats_persist_threshold";
         /** {@hide} */
-        public static final String NETSTATS_SUMMARY_BUCKET_DURATION = "netstats_summary_bucket_duration";
+        public static final String NETSTATS_NETWORK_BUCKET_DURATION = "netstats_network_bucket_duration";
         /** {@hide} */
-        public static final String NETSTATS_SUMMARY_MAX_HISTORY = "netstats_summary_max_history";
+        public static final String NETSTATS_NETWORK_MAX_HISTORY = "netstats_network_max_history";
         /** {@hide} */
-        public static final String NETSTATS_DETAIL_BUCKET_DURATION = "netstats_detail_bucket_duration";
+        public static final String NETSTATS_UID_BUCKET_DURATION = "netstats_uid_bucket_duration";
         /** {@hide} */
-        public static final String NETSTATS_DETAIL_MAX_HISTORY = "netstats_detail_max_history";
+        public static final String NETSTATS_UID_MAX_HISTORY = "netstats_uid_max_history";
 
         /**
          * @hide
diff --git a/services/java/com/android/server/net/NetworkStatsService.java b/services/java/com/android/server/net/NetworkStatsService.java
index 161c393..f5d01d6 100644
--- a/services/java/com/android/server/net/NetworkStatsService.java
+++ b/services/java/com/android/server/net/NetworkStatsService.java
@@ -23,12 +23,12 @@
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.NetworkStats.IFACE_ALL;
 import static android.net.NetworkStats.UID_ALL;
-import static android.provider.Settings.Secure.NETSTATS_DETAIL_BUCKET_DURATION;
-import static android.provider.Settings.Secure.NETSTATS_DETAIL_MAX_HISTORY;
+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_POLL_INTERVAL;
-import static android.provider.Settings.Secure.NETSTATS_SUMMARY_BUCKET_DURATION;
-import static android.provider.Settings.Secure.NETSTATS_SUMMARY_MAX_HISTORY;
+import static android.provider.Settings.Secure.NETSTATS_UID_BUCKET_DURATION;
+import static android.provider.Settings.Secure.NETSTATS_UID_MAX_HISTORY;
 import static android.text.format.DateUtils.DAY_IN_MILLIS;
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
@@ -38,6 +38,7 @@
 import android.app.IAlarmManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -88,6 +89,7 @@
 public class NetworkStatsService extends INetworkStatsService.Stub {
     private static final String TAG = "NetworkStats";
     private static final boolean LOGD = true;
+    private static final boolean LOGV = false;
 
     /** File header magic number: "ANET" */
     private static final int FILE_MAGIC = 0x414E4554;
@@ -97,6 +99,7 @@
     private final INetworkManagementService mNetworkManager;
     private final IAlarmManager mAlarmManager;
     private final TrustedTime mTime;
+    private final NetworkStatsSettings mSettings;
 
     private IConnectivityManager mConnManager;
 
@@ -112,22 +115,18 @@
     private static final long MB_IN_BYTES = 1024 * KB_IN_BYTES;
     private static final long GB_IN_BYTES = 1024 * MB_IN_BYTES;
 
-    private LongSecureSetting mPollInterval = new LongSecureSetting(
-            NETSTATS_POLL_INTERVAL, 15 * MINUTE_IN_MILLIS);
-    private LongSecureSetting mPersistThreshold = new LongSecureSetting(
-            NETSTATS_PERSIST_THRESHOLD, 16 * KB_IN_BYTES);
-
-    // TODO: adjust these timings for production builds
-    private LongSecureSetting mSummaryBucketDuration = new LongSecureSetting(
-            NETSTATS_SUMMARY_BUCKET_DURATION, 1 * HOUR_IN_MILLIS);
-    private LongSecureSetting mSummaryMaxHistory = new LongSecureSetting(
-            NETSTATS_SUMMARY_MAX_HISTORY, 90 * DAY_IN_MILLIS);
-    private LongSecureSetting mDetailBucketDuration = new LongSecureSetting(
-            NETSTATS_DETAIL_BUCKET_DURATION, 2 * HOUR_IN_MILLIS);
-    private LongSecureSetting mDetailMaxHistory = new LongSecureSetting(
-            NETSTATS_DETAIL_MAX_HISTORY, 90 * DAY_IN_MILLIS);
-
-    private static final long TIME_CACHE_MAX_AGE = DAY_IN_MILLIS;
+    /**
+     * 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 getTimeCacheMaxAge();
+    }
 
     private final Object mStatsLock = new Object();
 
@@ -135,19 +134,23 @@
     private HashMap<String, InterfaceIdentity> mActiveIface = Maps.newHashMap();
 
     /** Set of historical stats for known ifaces. */
-    private HashMap<InterfaceIdentity, NetworkStatsHistory> mSummaryStats = Maps.newHashMap();
+    private HashMap<InterfaceIdentity, NetworkStatsHistory> mNetworkStats = Maps.newHashMap();
     /** Set of historical stats for known UIDs. */
-    private SparseArray<NetworkStatsHistory> mDetailStats = new SparseArray<NetworkStatsHistory>();
+    private SparseArray<NetworkStatsHistory> mUidStats = new SparseArray<NetworkStatsHistory>();
 
-    private NetworkStats mLastSummaryPoll;
-    private NetworkStats mLastSummaryPersist;
+    /** Flag if {@link #mUidStats} have been loaded from disk. */
+    private boolean mUidStatsLoaded = false;
 
-    private NetworkStats mLastDetailPoll;
+    private NetworkStats mLastNetworkPoll;
+    private NetworkStats mLastNetworkPersist;
+
+    private NetworkStats mLastUidPoll;
 
     private final HandlerThread mHandlerThread;
     private final Handler mHandler;
 
-    private final AtomicFile mSummaryFile;
+    private final AtomicFile mNetworkFile;
+    private final AtomicFile mUidFile;
 
     // TODO: collect detailed uid stats, storing tag-granularity data until next
     // dropbox, and uid summary for a specific bucket count.
@@ -157,7 +160,8 @@
     public NetworkStatsService(
             Context context, INetworkManagementService networkManager, IAlarmManager alarmManager) {
         // TODO: move to using cached NtpTrustedTime
-        this(context, networkManager, alarmManager, new NtpTrustedTime(), getSystemDir());
+        this(context, networkManager, alarmManager, new NtpTrustedTime(), getSystemDir(),
+                new DefaultNetworkStatsSettings(context));
     }
 
     private static File getSystemDir() {
@@ -165,17 +169,20 @@
     }
 
     public NetworkStatsService(Context context, INetworkManagementService networkManager,
-            IAlarmManager alarmManager, TrustedTime time, File systemDir) {
+            IAlarmManager alarmManager, TrustedTime time, File systemDir,
+            NetworkStatsSettings settings) {
         mContext = checkNotNull(context, "missing Context");
         mNetworkManager = checkNotNull(networkManager, "missing INetworkManagementService");
         mAlarmManager = checkNotNull(alarmManager, "missing IAlarmManager");
         mTime = checkNotNull(time, "missing TrustedTime");
+        mSettings = checkNotNull(settings, "missing NetworkStatsSettings");
 
         mHandlerThread = new HandlerThread(TAG);
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
 
-        mSummaryFile = new AtomicFile(new File(systemDir, "netstats.bin"));
+        mNetworkFile = new AtomicFile(new File(systemDir, "netstats.bin"));
+        mUidFile = new AtomicFile(new File(systemDir, "netstats_uid.bin"));
     }
 
     public void bindConnectivityManager(IConnectivityManager connManager) {
@@ -184,8 +191,10 @@
 
     public void systemReady() {
         synchronized (mStatsLock) {
-            // read historical stats from disk
-            readStatsLocked();
+            // read historical network stats from disk, since policy service
+            // might need them right away. we delay loading detailed UID stats
+            // until actually needed.
+            readNetworkStatsLocked();
         }
 
         // watch for network interfaces to be claimed
@@ -214,14 +223,16 @@
         mContext.unregisterReceiver(mPollReceiver);
         mContext.unregisterReceiver(mShutdownReceiver);
 
-        writeStatsLocked();
-        mSummaryStats.clear();
-        mDetailStats.clear();
+        writeNetworkStatsLocked();
+        writeUidStatsLocked();
+        mNetworkStats.clear();
+        mUidStats.clear();
+        mUidStatsLoaded = false;
     }
 
     /**
      * Clear any existing {@link #ACTION_NETWORK_STATS_POLL} alarms, and
-     * reschedule based on current {@link #mPollInterval} value.
+     * reschedule based on current {@link NetworkStatsSettings#getPollInterval()}.
      */
     private void registerPollAlarmLocked() throws RemoteException {
         if (mPollIntent != null) {
@@ -232,8 +243,8 @@
                 mContext, 0, new Intent(ACTION_NETWORK_STATS_POLL), 0);
 
         final long currentRealtime = SystemClock.elapsedRealtime();
-        mAlarmManager.setInexactRepeating(
-                AlarmManager.ELAPSED_REALTIME, currentRealtime, mPollInterval.get(), mPollIntent);
+        mAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, currentRealtime,
+                mSettings.getPollInterval(), mPollIntent);
     }
 
     @Override
@@ -244,9 +255,9 @@
             // combine all interfaces that match template
             final String subscriberId = getActiveSubscriberId();
             final NetworkStatsHistory combined = new NetworkStatsHistory(
-                    mSummaryBucketDuration.get());
-            for (InterfaceIdentity ident : mSummaryStats.keySet()) {
-                final NetworkStatsHistory history = mSummaryStats.get(ident);
+                    mSettings.getNetworkBucketDuration());
+            for (InterfaceIdentity ident : mNetworkStats.keySet()) {
+                final NetworkStatsHistory history = mNetworkStats.get(ident);
                 if (ident.matchesTemplate(networkTemplate, subscriberId)) {
                     combined.recordEntireHistory(history);
                 }
@@ -259,8 +270,11 @@
     public NetworkStatsHistory getHistoryForUid(int uid, int networkTemplate) {
         mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG);
 
-        // TODO: return history for requested uid
-        return null;
+        synchronized (mStatsLock) {
+            // TODO: combine based on template, if we store that granularity
+            ensureUidStatsLoadedLocked();
+            return mUidStats.get(uid);
+        }
     }
 
     @Override
@@ -274,8 +288,8 @@
             long[] networkTotal = new long[2];
 
             // combine total from all interfaces that match template
-            for (InterfaceIdentity ident : mSummaryStats.keySet()) {
-                final NetworkStatsHistory history = mSummaryStats.get(ident);
+            for (InterfaceIdentity ident : mNetworkStats.keySet()) {
+                final NetworkStatsHistory history = mNetworkStats.get(ident);
                 if (ident.matchesTemplate(networkTemplate, subscriberId)) {
                     networkTotal = history.getTotalData(start, end, networkTotal);
                     rx += networkTotal[0];
@@ -296,13 +310,15 @@
         // TODO: apply networktemplate once granular uid stats are stored.
 
         synchronized (mStatsLock) {
-            final int size = mDetailStats.size();
+            ensureUidStatsLoadedLocked();
+
+            final int size = mUidStats.size();
             final NetworkStats.Builder stats = new NetworkStats.Builder(end - start, size);
 
             long[] total = new long[2];
             for (int i = 0; i < size; i++) {
-                final int uid = mDetailStats.keyAt(i);
-                final NetworkStatsHistory history = mDetailStats.valueAt(i);
+                final int uid = mUidStats.keyAt(i);
+                final NetworkStatsHistory history = mUidStats.valueAt(i);
                 total = history.getTotalData(start, end, total);
                 stats.addEntry(IFACE_ALL, uid, total[0], total[1]);
             }
@@ -333,7 +349,7 @@
             // permission above.
             synchronized (mStatsLock) {
                 // TODO: acquire wakelock while performing poll
-                performPollLocked();
+                performPollLocked(true);
             }
         }
     };
@@ -355,13 +371,12 @@
      * {@link InterfaceIdentity}.
      */
     private void updateIfacesLocked() {
-        if (LOGD) Slog.v(TAG, "updateIfacesLocked()");
+        if (LOGV) Slog.v(TAG, "updateIfacesLocked()");
 
         // take one last stats snapshot before updating iface mapping. this
         // isn't perfect, since the kernel may already be counting traffic from
         // the updated network.
-        // TODO: verify that we only poll summary stats, not uid details
-        performPollLocked();
+        performPollLocked(false);
 
         final NetworkState[] states;
         try {
@@ -384,11 +399,18 @@
         }
     }
 
-    private void performPollLocked() {
-        if (LOGD) Slog.v(TAG, "performPollLocked()");
+    /**
+     * Periodic poll operation, reading current statistics and recording into
+     * {@link NetworkStatsHistory}.
+     *
+     * @param detailedPoll Indicate if detailed UID stats should be collected
+     *            during this poll operation.
+     */
+    private void performPollLocked(boolean detailedPoll) {
+        if (LOGV) Slog.v(TAG, "performPollLocked()");
 
         // try refreshing time source when stale
-        if (mTime.getCacheAge() > TIME_CACHE_MAX_AGE) {
+        if (mTime.getCacheAge() > mSettings.getTimeCacheMaxAge()) {
             mTime.forceRefresh();
         }
 
@@ -396,42 +418,45 @@
         final long currentTime = mTime.hasCache() ? mTime.currentTimeMillis()
                 : System.currentTimeMillis();
 
-        final NetworkStats summary;
-        final NetworkStats detail;
+        final NetworkStats networkStats;
+        final NetworkStats uidStats;
         try {
-            summary = mNetworkManager.getNetworkStatsSummary();
-            detail = mNetworkManager.getNetworkStatsDetail();
+            networkStats = mNetworkManager.getNetworkStatsSummary();
+            uidStats = detailedPoll ? mNetworkManager.getNetworkStatsDetail() : null;
         } catch (RemoteException e) {
             Slog.w(TAG, "problem reading network stats");
             return;
         }
 
-        performSummaryPollLocked(summary, currentTime);
-        performDetailPollLocked(detail, currentTime);
+        performNetworkPollLocked(networkStats, currentTime);
+        if (detailedPoll) {
+            performUidPollLocked(uidStats, currentTime);
+        }
 
         // decide if enough has changed to trigger persist
-        final NetworkStats persistDelta = computeStatsDelta(mLastSummaryPersist, summary);
-        final long persistThreshold = mPersistThreshold.get();
+        final NetworkStats persistDelta = computeStatsDelta(mLastNetworkPersist, networkStats);
+        final long persistThreshold = mSettings.getPersistThreshold();
         for (String iface : persistDelta.getUniqueIfaces()) {
             final int index = persistDelta.findIndex(iface, UID_ALL);
             if (persistDelta.rx[index] > persistThreshold
                     || persistDelta.tx[index] > persistThreshold) {
-                writeStatsLocked();
-                mLastSummaryPersist = summary;
+                writeNetworkStatsLocked();
+                writeUidStatsLocked();
+                mLastNetworkPersist = networkStats;
                 break;
             }
         }
     }
 
     /**
-     * Update {@link #mSummaryStats} historical usage.
+     * Update {@link #mNetworkStats} historical usage.
      */
-    private void performSummaryPollLocked(NetworkStats summary, long currentTime) {
+    private void performNetworkPollLocked(NetworkStats networkStats, long currentTime) {
         final ArrayList<String> unknownIface = Lists.newArrayList();
 
-        final NetworkStats delta = computeStatsDelta(mLastSummaryPoll, summary);
+        final NetworkStats delta = computeStatsDelta(mLastNetworkPoll, networkStats);
         final long timeStart = currentTime - delta.elapsedRealtime;
-        final long maxHistory = mSummaryMaxHistory.get();
+        final long maxHistory = mSettings.getNetworkMaxHistory();
         for (String iface : delta.getUniqueIfaces()) {
             final InterfaceIdentity ident = mActiveIface.get(iface);
             if (ident == null) {
@@ -443,11 +468,11 @@
             final long rx = delta.rx[index];
             final long tx = delta.tx[index];
 
-            final NetworkStatsHistory history = findOrCreateSummaryLocked(ident);
+            final NetworkStatsHistory history = findOrCreateNetworkLocked(ident);
             history.recordData(timeStart, currentTime, rx, tx);
             history.removeBucketsBefore(currentTime - maxHistory);
         }
-        mLastSummaryPoll = summary;
+        mLastNetworkPoll = networkStats;
 
         if (LOGD && unknownIface.size() > 0) {
             Slog.w(TAG, "unknown interfaces " + unknownIface.toString() + ", ignoring those stats");
@@ -455,40 +480,69 @@
     }
 
     /**
-     * Update {@link #mDetailStats} historical usage.
+     * Update {@link #mUidStats} historical usage.
      */
-    private void performDetailPollLocked(NetworkStats detail, long currentTime) {
-        final NetworkStats delta = computeStatsDelta(mLastDetailPoll, detail);
+    private void performUidPollLocked(NetworkStats uidStats, long currentTime) {
+        ensureUidStatsLoadedLocked();
+
+        final NetworkStats delta = computeStatsDelta(mLastUidPoll, uidStats);
         final long timeStart = currentTime - delta.elapsedRealtime;
-        final long maxHistory = mDetailMaxHistory.get();
+        final long maxHistory = mSettings.getUidMaxHistory();
         for (int uid : delta.getUniqueUids()) {
+            // TODO: traverse all ifaces once surfaced in stats
             final int index = delta.findIndex(IFACE_ALL, uid);
-            final long rx = delta.rx[index];
-            final long tx = delta.tx[index];
+            if (index != -1) {
+                final long rx = delta.rx[index];
+                final long tx = delta.tx[index];
 
-            final NetworkStatsHistory history = findOrCreateDetailLocked(uid);
-            history.recordData(timeStart, currentTime, rx, tx);
-            history.removeBucketsBefore(currentTime - maxHistory);
+                final NetworkStatsHistory history = findOrCreateUidLocked(uid);
+                history.recordData(timeStart, currentTime, rx, tx);
+                history.removeBucketsBefore(currentTime - maxHistory);
+            }
         }
-        mLastDetailPoll = detail;
+        mLastUidPoll = uidStats;
     }
 
-    private NetworkStatsHistory findOrCreateSummaryLocked(InterfaceIdentity ident) {
-        NetworkStatsHistory stats = mSummaryStats.get(ident);
-        if (stats == null) {
-            stats = new NetworkStatsHistory(mSummaryBucketDuration.get());
-            mSummaryStats.put(ident, stats);
+    private NetworkStatsHistory findOrCreateNetworkLocked(InterfaceIdentity ident) {
+        final long bucketDuration = mSettings.getNetworkBucketDuration();
+        final NetworkStatsHistory existing = mNetworkStats.get(ident);
+
+        // update when no existing, or when bucket duration changed
+        NetworkStatsHistory updated = null;
+        if (existing == null) {
+            updated = new NetworkStatsHistory(bucketDuration);
+        } else if (existing.bucketDuration != bucketDuration) {
+            updated = new NetworkStatsHistory(bucketDuration);
+            updated.recordEntireHistory(existing);
         }
-        return stats;
+
+        if (updated != null) {
+            mNetworkStats.put(ident, updated);
+            return updated;
+        } else {
+            return existing;
+        }
     }
 
-    private NetworkStatsHistory findOrCreateDetailLocked(int uid) {
-        NetworkStatsHistory stats = mDetailStats.get(uid);
-        if (stats == null) {
-            stats = new NetworkStatsHistory(mDetailBucketDuration.get());
-            mDetailStats.put(uid, stats);
+    private NetworkStatsHistory findOrCreateUidLocked(int uid) {
+        final long bucketDuration = mSettings.getUidBucketDuration();
+        final NetworkStatsHistory existing = mUidStats.get(uid);
+
+        // update when no existing, or when bucket duration changed
+        NetworkStatsHistory updated = null;
+        if (existing == null) {
+            updated = new NetworkStatsHistory(bucketDuration);
+        } else if (existing.bucketDuration != bucketDuration) {
+            updated = new NetworkStatsHistory(bucketDuration);
+            updated.recordEntireHistory(existing);
         }
-        return stats;
+
+        if (updated != null) {
+            mUidStats.put(uid, updated);
+            return updated;
+        } else {
+            return existing;
+        }
     }
 
     private InterfaceIdentity findOrCreateInterfaceLocked(String iface) {
@@ -500,15 +554,15 @@
         return ident;
     }
 
-    private void readStatsLocked() {
-        if (LOGD) Slog.v(TAG, "readStatsLocked()");
+    private void readNetworkStatsLocked() {
+        if (LOGV) Slog.v(TAG, "readNetworkStatsLocked()");
 
         // clear any existing stats and read from disk
-        mSummaryStats.clear();
+        mNetworkStats.clear();
 
         FileInputStream fis = null;
         try {
-            fis = mSummaryFile.openRead();
+            fis = mNetworkFile.openRead();
             final DataInputStream in = new DataInputStream(fis);
 
             // verify file magic header intact
@@ -521,13 +575,14 @@
             switch (version) {
                 case VERSION_CURRENT: {
                     // file format is pairs of interfaces and stats:
-                    // summary := size *(InterfaceIdentity NetworkStatsHistory)
+                    // network := size *(InterfaceIdentity NetworkStatsHistory)
 
                     final int size = in.readInt();
                     for (int i = 0; i < size; i++) {
                         final InterfaceIdentity ident = new InterfaceIdentity(in);
                         final NetworkStatsHistory history = new NetworkStatsHistory(in);
-                        mSummaryStats.put(ident, history);
+
+                        mNetworkStats.put(ident, history);
                     }
                     break;
                 }
@@ -544,30 +599,113 @@
         }
     }
 
-    private void writeStatsLocked() {
-        if (LOGD) Slog.v(TAG, "writeStatsLocked()");
+    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();
+
+        FileInputStream fis = null;
+        try {
+            fis = mUidFile.openRead();
+            final DataInputStream in = new DataInputStream(fis);
+
+            // 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_CURRENT: {
+                    // file format is pairs of UIDs and stats:
+                    // uid := size *(UID NetworkStatsHistory)
+
+                    final int size = in.readInt();
+                    for (int i = 0; i < size; i++) {
+                        final int uid = in.readInt();
+                        final NetworkStatsHistory history = new NetworkStatsHistory(in);
+
+                        mUidStats.put(uid, history);
+                    }
+                    break;
+                }
+                default: {
+                    throw new ProtocolException("unexpected version: " + version);
+                }
+            }
+        } catch (FileNotFoundException e) {
+            // missing stats is okay, probably first boot
+        } catch (IOException e) {
+            Slog.e(TAG, "problem reading uid stats", e);
+        } finally {
+            IoUtils.closeQuietly(fis);
+        }
+    }
+
+    private void writeNetworkStatsLocked() {
+        if (LOGV) Slog.v(TAG, "writeNetworkStatsLocked()");
 
         // TODO: consider duplicating stats and releasing lock while writing
 
         FileOutputStream fos = null;
         try {
-            fos = mSummaryFile.startWrite();
+            fos = mNetworkFile.startWrite();
             final DataOutputStream out = new DataOutputStream(fos);
 
             out.writeInt(FILE_MAGIC);
             out.writeInt(VERSION_CURRENT);
 
-            out.writeInt(mSummaryStats.size());
-            for (InterfaceIdentity ident : mSummaryStats.keySet()) {
-                final NetworkStatsHistory history = mSummaryStats.get(ident);
+            out.writeInt(mNetworkStats.size());
+            for (InterfaceIdentity ident : mNetworkStats.keySet()) {
+                final NetworkStatsHistory history = mNetworkStats.get(ident);
                 ident.writeToStream(out);
                 history.writeToStream(out);
             }
 
-            mSummaryFile.finishWrite(fos);
+            mNetworkFile.finishWrite(fos);
         } catch (IOException e) {
             if (fos != null) {
-                mSummaryFile.failWrite(fos);
+                mNetworkFile.failWrite(fos);
+            }
+        }
+    }
+
+    private void writeUidStatsLocked() {
+        if (LOGV) Slog.v(TAG, "writeUidStatsLocked()");
+
+        // TODO: consider duplicating stats and releasing lock while writing
+
+        FileOutputStream fos = null;
+        try {
+            fos = mUidFile.startWrite();
+            final DataOutputStream out = new DataOutputStream(fos);
+
+            out.writeInt(FILE_MAGIC);
+            out.writeInt(VERSION_CURRENT);
+
+            final int size = mUidStats.size();
+
+            out.writeInt(size);
+            for (int i = 0; i < size; i++) {
+                final int uid = mUidStats.keyAt(i);
+                final NetworkStatsHistory history = mUidStats.valueAt(i);
+                out.writeInt(uid);
+                history.writeToStream(out);
+            }
+
+            mUidFile.finishWrite(fos);
+        } catch (IOException e) {
+            if (fos != null) {
+                mUidFile.failWrite(fos);
             }
         }
     }
@@ -590,7 +728,7 @@
             }
 
             if (argSet.contains("poll")) {
-                performPollLocked();
+                performPollLocked(true);
                 pw.println("Forced poll");
                 return;
             }
@@ -603,17 +741,20 @@
             }
 
             pw.println("Known historical stats:");
-            for (InterfaceIdentity ident : mSummaryStats.keySet()) {
-                final NetworkStatsHistory stats = mSummaryStats.get(ident);
+            for (InterfaceIdentity ident : mNetworkStats.keySet()) {
+                final NetworkStatsHistory stats = mNetworkStats.get(ident);
                 pw.print("  ident="); pw.println(ident.toString());
                 stats.dump("    ", pw);
             }
 
             if (argSet.contains("detail")) {
-                pw.println("Known detail stats:");
-                for (int i = 0; i < mDetailStats.size(); i++) {
-                    final int uid = mDetailStats.keyAt(i);
-                    final NetworkStatsHistory stats = mDetailStats.valueAt(i);
+                // since explicitly requested with argument, we're okay to load
+                // from disk if not already in memory.
+                ensureUidStatsLoadedLocked();
+                pw.println("Known UID stats:");
+                for (int i = 0; i < mUidStats.size(); i++) {
+                    final int uid = mUidStats.keyAt(i);
+                    final NetworkStatsHistory stats = mUidStats.valueAt(i);
                     pw.print("  UID="); pw.println(uid);
                     stats.dump("    ", pw);
                 }
@@ -627,47 +768,29 @@
     @Deprecated
     private void generateRandomLocked() {
         long end = System.currentTimeMillis();
-        long start = end - mSummaryMaxHistory.get();
+        long start = end - mSettings.getNetworkMaxHistory();
         long rx = 3 * GB_IN_BYTES;
         long tx = 2 * GB_IN_BYTES;
 
-        mSummaryStats.clear();
+        mNetworkStats.clear();
         for (InterfaceIdentity ident : mActiveIface.values()) {
-            final NetworkStatsHistory stats = findOrCreateSummaryLocked(ident);
+            final NetworkStatsHistory stats = findOrCreateNetworkLocked(ident);
             stats.generateRandom(start, end, rx, tx);
         }
 
         end = System.currentTimeMillis();
-        start = end - mDetailMaxHistory.get();
+        start = end - mSettings.getUidMaxHistory();
         rx = 500 * MB_IN_BYTES;
         tx = 100 * MB_IN_BYTES;
 
-        mDetailStats.clear();
+        mUidStats.clear();
         for (ApplicationInfo info : mContext.getPackageManager().getInstalledApplications(0)) {
             final int uid = info.uid;
-            final NetworkStatsHistory stats = findOrCreateDetailLocked(uid);
+            final NetworkStatsHistory stats = findOrCreateUidLocked(uid);
             stats.generateRandom(start, end, rx, tx);
         }
     }
 
-    private class LongSecureSetting {
-        private String mKey;
-        private long mDefaultValue;
-
-        public LongSecureSetting(String key, long defaultValue) {
-            mKey = key;
-            mDefaultValue = defaultValue;
-        }
-
-        public long get() {
-            if (mContext != null) {
-                return Settings.Secure.getLong(mContext.getContentResolver(), mKey, mDefaultValue);
-            } else {
-                return mDefaultValue;
-            }
-        }
-    }
-
     /**
      * Return the delta between two {@link NetworkStats} snapshots, where {@code
      * before} can be {@code null}.
@@ -686,4 +809,42 @@
         return telephony.getSubscriberId();
     }
 
+    /**
+     * Default external settings that read from {@link Settings.Secure}.
+     */
+    private static class DefaultNetworkStatsSettings implements NetworkStatsSettings {
+        private final ContentResolver mResolver;
+
+        public DefaultNetworkStatsSettings(Context context) {
+            mResolver = checkNotNull(context.getContentResolver());
+            // TODO: adjust these timings for production builds
+        }
+
+        private long getSecureLong(String name, long def) {
+            return Settings.Secure.getLong(mResolver, name, def);
+        }
+
+        public long getPollInterval() {
+            return getSecureLong(NETSTATS_POLL_INTERVAL, 15 * MINUTE_IN_MILLIS);
+        }
+        public long getPersistThreshold() {
+            return getSecureLong(NETSTATS_PERSIST_THRESHOLD, 16 * KB_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 getTimeCacheMaxAge() {
+            return DAY_IN_MILLIS;
+        }
+    }
+
 }
diff --git a/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java
index 9846372..d6e4b8b 100644
--- a/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java
@@ -18,10 +18,13 @@
 
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.NetworkStats.IFACE_ALL;
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.TrafficStats.TEMPLATE_WIFI;
 import static android.text.format.DateUtils.DAY_IN_MILLIS;
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+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.createMock;
@@ -47,6 +50,7 @@
 import android.util.TrustedTime;
 
 import com.android.server.net.NetworkStatsService;
+import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
 
 import org.easymock.EasyMock;
 
@@ -62,12 +66,16 @@
     private static final String TEST_IFACE = "test0";
     private static final long TEST_START = 1194220800000L;
 
+    private static final int TEST_UID_1 = 1001;
+    private static final int TEST_UID_2 = 1002;
+
     private BroadcastInterceptingContext mServiceContext;
     private File mStatsDir;
 
     private INetworkManagementService mNetManager;
     private IAlarmManager mAlarmManager;
     private TrustedTime mTime;
+    private NetworkStatsSettings mSettings;
     private IConnectivityManager mConnManager;
 
     private NetworkStatsService mService;
@@ -82,12 +90,14 @@
         mNetManager = createMock(INetworkManagementService.class);
         mAlarmManager = createMock(IAlarmManager.class);
         mTime = createMock(TrustedTime.class);
+        mSettings = createMock(NetworkStatsSettings.class);
         mConnManager = createMock(IConnectivityManager.class);
 
         mService = new NetworkStatsService(
-                mServiceContext, mNetManager, mAlarmManager, mTime, mStatsDir);
+                mServiceContext, mNetManager, mAlarmManager, mTime, mStatsDir, mSettings);
         mService.bindConnectivityManager(mConnManager);
 
+        expectDefaultSettings();
         expectSystemReady();
 
         replay();
@@ -114,115 +124,93 @@
         super.tearDown();
     }
 
-    private static NetworkState buildWifi() {
-        final NetworkInfo info = new NetworkInfo(TYPE_WIFI, 0, null, null);
-        info.setDetailedState(DetailedState.CONNECTED, null, null);
-        final LinkProperties prop = new LinkProperties();
-        prop.setInterfaceName(TEST_IFACE);
-        return new NetworkState(info, prop, null);
-    }
-
-    public void testHistoryForWifi() throws Exception {
+    public void testSummaryStatsWifi() throws Exception {
         long elapsedRealtime = 0;
-        NetworkState[] state = null;
-        NetworkStats stats = null;
-        NetworkStats detail = null;
 
         // pretend that wifi network comes online; service should ask about full
         // network state, and poll any existing interfaces before updating.
-        state = new NetworkState[] { buildWifi() };
-        stats = new NetworkStats.Builder(elapsedRealtime, 0).build();
-        detail = new NetworkStats.Builder(elapsedRealtime, 0).build();
-
-        expect(mConnManager.getAllNetworkState()).andReturn(state).atLeastOnce();
-        expect(mNetManager.getNetworkStatsSummary()).andReturn(stats).atLeastOnce();
-        expect(mNetManager.getNetworkStatsDetail()).andReturn(detail).atLeastOnce();
         expectTime(TEST_START + elapsedRealtime);
+        expectDefaultSettings();
+        expectNetworkState(buildWifiState());
+        expectNetworkStatsSummary(buildEmptyStats(elapsedRealtime));
 
         replay();
         mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION));
-        verifyAndReset();
 
         // verify service has empty history for wifi
         assertNetworkTotal(TEMPLATE_WIFI, 0L, 0L);
+        verifyAndReset();
 
         // modify some number on wifi, and trigger poll event
         elapsedRealtime += HOUR_IN_MILLIS;
-        stats = new NetworkStats.Builder(elapsedRealtime, 1).addEntry(
-                TEST_IFACE, UID_ALL, 1024L, 2048L).build();
-
-        expect(mNetManager.getNetworkStatsSummary()).andReturn(stats).atLeastOnce();
-        expect(mNetManager.getNetworkStatsDetail()).andReturn(detail).atLeastOnce();
         expectTime(TEST_START + elapsedRealtime);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats.Builder(elapsedRealtime, 1)
+                .addEntry(TEST_IFACE, UID_ALL, 1024L, 2048L).build());
+        expectNetworkStatsDetail(buildEmptyStats(elapsedRealtime));
 
         replay();
         mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
-        verifyAndReset();
 
         // verify service recorded history
         assertNetworkTotal(TEMPLATE_WIFI, 1024L, 2048L);
+        verifyAndReset();
 
         // and bump forward again, with counters going higher. this is
         // important, since polling should correctly subtract last snapshot.
         elapsedRealtime += DAY_IN_MILLIS;
-        stats = new NetworkStats.Builder(elapsedRealtime, 1).addEntry(
-                TEST_IFACE, UID_ALL, 4096L, 8192L).build();
-
-        expect(mNetManager.getNetworkStatsSummary()).andReturn(stats).atLeastOnce();
-        expect(mNetManager.getNetworkStatsDetail()).andReturn(detail).atLeastOnce();
         expectTime(TEST_START + elapsedRealtime);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats.Builder(elapsedRealtime, 1)
+                .addEntry(TEST_IFACE, UID_ALL, 4096L, 8192L).build());
+        expectNetworkStatsDetail(buildEmptyStats(elapsedRealtime));
 
         replay();
         mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
-        verifyAndReset();
 
         // verify service recorded history
         assertNetworkTotal(TEMPLATE_WIFI, 4096L, 8192L);
+        verifyAndReset();
+
     }
 
-    public void testHistoryForRebootPersist() throws Exception {
+    public void testStatsRebootPersist() throws Exception {
         long elapsedRealtime = 0;
-        NetworkState[] state = null;
-        NetworkStats stats = null;
-        NetworkStats detail = null;
-
-        // assert that no stats file exists
-        final File statsFile = new File(mStatsDir, "netstats.bin");
-        assertFalse(statsFile.exists());
+        assertStatsFilesExist(false);
 
         // pretend that wifi network comes online; service should ask about full
         // network state, and poll any existing interfaces before updating.
-        state = new NetworkState[] { buildWifi() };
-        stats = new NetworkStats.Builder(elapsedRealtime, 0).build();
-        detail = new NetworkStats.Builder(elapsedRealtime, 0).build();
-
-        expect(mConnManager.getAllNetworkState()).andReturn(state).atLeastOnce();
-        expect(mNetManager.getNetworkStatsSummary()).andReturn(stats).atLeastOnce();
-        expect(mNetManager.getNetworkStatsDetail()).andReturn(detail).atLeastOnce();
         expectTime(TEST_START + elapsedRealtime);
+        expectDefaultSettings();
+        expectNetworkState(buildWifiState());
+        expectNetworkStatsSummary(buildEmptyStats(elapsedRealtime));
 
         replay();
         mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION));
-        verifyAndReset();
 
         // verify service has empty history for wifi
         assertNetworkTotal(TEMPLATE_WIFI, 0L, 0L);
+        verifyAndReset();
 
         // modify some number on wifi, and trigger poll event
         elapsedRealtime += HOUR_IN_MILLIS;
-        stats = new NetworkStats.Builder(elapsedRealtime, 1).addEntry(
-                TEST_IFACE, UID_ALL, 1024L, 2048L).build();
-
-        expect(mNetManager.getNetworkStatsSummary()).andReturn(stats).atLeastOnce();
-        expect(mNetManager.getNetworkStatsDetail()).andReturn(detail).atLeastOnce();
         expectTime(TEST_START + elapsedRealtime);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats.Builder(elapsedRealtime, 1)
+                .addEntry(TEST_IFACE, UID_ALL, 1024L, 2048L).build());
+        // TODO: switch these stats to specific iface
+        expectNetworkStatsDetail(new NetworkStats.Builder(elapsedRealtime, 2)
+                .addEntry(IFACE_ALL, TEST_UID_1, 512L, 256L)
+                .addEntry(IFACE_ALL, TEST_UID_2, 128L, 128L).build());
 
         replay();
         mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
-        verifyAndReset();
 
         // verify service recorded history
         assertNetworkTotal(TEMPLATE_WIFI, 1024L, 2048L);
+        assertUidTotal(TEST_UID_1, TEMPLATE_WIFI, 512L, 256L);
+        assertUidTotal(TEST_UID_2, TEMPLATE_WIFI, 128L, 128L);
+        verifyAndReset();
 
         // graceful shutdown system, which should trigger persist of stats, and
         // clear any values in memory.
@@ -230,18 +218,84 @@
 
         // talk with zombie service to assert stats have gone; and assert that
         // we persisted them to file.
+        expectDefaultSettings();
+        replay();
         assertNetworkTotal(TEMPLATE_WIFI, 0L, 0L);
-        assertTrue(statsFile.exists());
+        verifyAndReset();
+
+        assertStatsFilesExist(true);
 
         // boot through serviceReady() again
+        expectDefaultSettings();
         expectSystemReady();
 
         replay();
         mService.systemReady();
-        verifyAndReset();
 
         // after systemReady(), we should have historical stats loaded again
         assertNetworkTotal(TEMPLATE_WIFI, 1024L, 2048L);
+        assertUidTotal(TEST_UID_1, TEMPLATE_WIFI, 512L, 256L);
+        assertUidTotal(TEST_UID_2, TEMPLATE_WIFI, 128L, 128L);
+        verifyAndReset();
+
+    }
+
+    public void testStatsBucketResize() throws Exception {
+        long elapsedRealtime = 0;
+        NetworkStatsHistory history = null;
+        long[] total = null;
+
+        assertStatsFilesExist(false);
+
+        // pretend that wifi network comes online; service should ask about full
+        // network state, and poll any existing interfaces before updating.
+        expectTime(TEST_START + elapsedRealtime);
+        expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+        expectNetworkState(buildWifiState());
+        expectNetworkStatsSummary(buildEmptyStats(elapsedRealtime));
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION));
+        verifyAndReset();
+
+        // modify some number on wifi, and trigger poll event
+        elapsedRealtime += 2 * HOUR_IN_MILLIS;
+        expectTime(TEST_START + elapsedRealtime);
+        expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+        expectNetworkStatsSummary(new NetworkStats.Builder(elapsedRealtime, 1)
+                .addEntry(TEST_IFACE, UID_ALL, 512L, 512L).build());
+        expectNetworkStatsDetail(buildEmptyStats(elapsedRealtime));
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify service recorded history
+        history = mService.getHistoryForNetwork(TEMPLATE_WIFI);
+        total = history.getTotalData(Long.MIN_VALUE, Long.MAX_VALUE, null);
+        assertEquals(512L, total[0]);
+        assertEquals(512L, total[1]);
+        assertEquals(HOUR_IN_MILLIS, history.bucketDuration);
+        assertEquals(2, history.bucketCount);
+        verifyAndReset();
+
+        // now change bucket duration setting and trigger another poll with
+        // exact same values, which should resize existing buckets.
+        expectTime(TEST_START + elapsedRealtime);
+        expectSettings(0L, 30 * MINUTE_IN_MILLIS, WEEK_IN_MILLIS);
+        expectNetworkStatsSummary(buildEmptyStats(elapsedRealtime));
+        expectNetworkStatsDetail(buildEmptyStats(elapsedRealtime));
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify identical stats, but spread across 4 buckets now
+        history = mService.getHistoryForNetwork(TEMPLATE_WIFI);
+        total = history.getTotalData(Long.MIN_VALUE, Long.MAX_VALUE, null);
+        assertEquals(512L, total[0]);
+        assertEquals(512L, total[1]);
+        assertEquals(30 * MINUTE_IN_MILLIS, history.bucketDuration);
+        assertEquals(4, history.bucketCount);
+        verifyAndReset();
 
     }
 
@@ -252,6 +306,13 @@
         assertEquals(tx, total[1]);
     }
 
+    private void assertUidTotal(int uid, int template, long rx, long tx) {
+        final NetworkStatsHistory history = mService.getHistoryForUid(uid, template);
+        final long[] total = history.getTotalData(Long.MIN_VALUE, Long.MAX_VALUE, null);
+        assertEquals(rx, total[0]);
+        assertEquals(tx, total[1]);
+    }
+
     private void expectSystemReady() throws Exception {
         mAlarmManager.remove(isA(PendingIntent.class));
         expectLastCall().anyTimes();
@@ -261,7 +322,34 @@
         expectLastCall().atLeastOnce();
     }
 
-    public void expectTime(long currentTime) throws Exception {
+    private void expectNetworkState(NetworkState... state) throws Exception {
+        expect(mConnManager.getAllNetworkState()).andReturn(state).atLeastOnce();
+    }
+
+    private void expectNetworkStatsSummary(NetworkStats summary) throws Exception {
+        expect(mNetManager.getNetworkStatsSummary()).andReturn(summary).atLeastOnce();
+    }
+
+    private void expectNetworkStatsDetail(NetworkStats detail) throws Exception {
+        expect(mNetManager.getNetworkStatsDetail()).andReturn(detail).atLeastOnce();
+    }
+
+    private void expectDefaultSettings() throws Exception {
+        expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+    }
+
+    private void expectSettings(long persistThreshold, long bucketDuration, long maxHistory)
+            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.getTimeCacheMaxAge()).andReturn(DAY_IN_MILLIS).anyTimes();
+    }
+
+    private void expectTime(long currentTime) throws Exception {
         expect(mTime.forceRefresh()).andReturn(false).anyTimes();
         expect(mTime.hasCache()).andReturn(true).anyTimes();
         expect(mTime.currentTimeMillis()).andReturn(currentTime).anyTimes();
@@ -269,12 +357,36 @@
         expect(mTime.getCacheCertainty()).andReturn(0L).anyTimes();
     }
 
+    private void assertStatsFilesExist(boolean exist) {
+        final File summaryFile = new File(mStatsDir, "netstats.bin");
+        final File detailFile = new File(mStatsDir, "netstats_uid.bin");
+        if (exist) {
+            assertTrue(summaryFile.exists());
+            assertTrue(detailFile.exists());
+        } else {
+            assertFalse(summaryFile.exists());
+            assertFalse(detailFile.exists());
+        }
+    }
+
+    private static NetworkState buildWifiState() {
+        final NetworkInfo info = new NetworkInfo(TYPE_WIFI, 0, null, null);
+        info.setDetailedState(DetailedState.CONNECTED, null, null);
+        final LinkProperties prop = new LinkProperties();
+        prop.setInterfaceName(TEST_IFACE);
+        return new NetworkState(info, prop, null);
+    }
+
+    private static NetworkStats buildEmptyStats(long elapsedRealtime) {
+        return new NetworkStats.Builder(elapsedRealtime, 0).build();
+    }
+
     private void replay() {
-        EasyMock.replay(mNetManager, mAlarmManager, mTime, mConnManager);
+        EasyMock.replay(mNetManager, mAlarmManager, mTime, mSettings, mConnManager);
     }
 
     private void verifyAndReset() {
-        EasyMock.verify(mNetManager, mAlarmManager, mTime, mConnManager);
-        EasyMock.reset(mNetManager, mAlarmManager, mTime, mConnManager);
+        EasyMock.verify(mNetManager, mAlarmManager, mTime, mSettings, mConnManager);
+        EasyMock.reset(mNetManager, mAlarmManager, mTime, mSettings, mConnManager);
     }
 }