Persist network stats using AtomicFile.

Implements read/write of network stats using AtomicFile, along with
magic number and versioning.  Stores in "/data/system/netstats.bin"
for now.  Tests to verify that stats are persisted across a simulated
reboot, and to verify that TEMPLATE_WIFI is working.

Fixed bug where kernel counters rolling backwards would cause negative
stats to be recorded; now we clamp deltas at 0.

Change-Id: I53bce26fc8fd3f4ab1e34ce135d302edfa34db34
diff --git a/core/java/android/net/INetworkStatsService.aidl b/core/java/android/net/INetworkStatsService.aidl
index d38d16c..d05c9d3 100644
--- a/core/java/android/net/INetworkStatsService.aidl
+++ b/core/java/android/net/INetworkStatsService.aidl
@@ -28,6 +28,6 @@
     NetworkStatsHistory getHistoryForUid(int uid, int networkTemplate);
 
     /** Return usage summary per UID for traffic that matches template. */
-    NetworkStats getSummaryPerUid(long start, long end, int networkTemplate);
+    NetworkStats getSummaryForAllUid(long start, long end, int networkTemplate);
 
 }
diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java
index ee415fa..6354e9a 100644
--- a/core/java/android/net/NetworkStats.java
+++ b/core/java/android/net/NetworkStats.java
@@ -158,10 +158,37 @@
      * between two snapshots in time. Assumes that statistics rows collect over
      * time, and that none of them have disappeared.
      *
+     * @throws IllegalArgumentException when given {@link NetworkStats} is
+     *             non-monotonic.
+     */
+    public NetworkStats subtract(NetworkStats value) {
+        return subtract(value, true, false);
+    }
+
+    /**
+     * Subtract the given {@link NetworkStats}, effectively leaving the delta
+     * between two snapshots in time. Assumes that statistics rows collect over
+     * time, and that none of them have disappeared.
+     * <p>
+     * Instead of throwing when counters are non-monotonic, this variant clamps
+     * results to never be negative.
+     */
+    public NetworkStats subtractClamped(NetworkStats value) {
+        return subtract(value, false, true);
+    }
+
+    /**
+     * Subtract the given {@link NetworkStats}, effectively leaving the delta
+     * between two snapshots in time. Assumes that statistics rows collect over
+     * time, and that none of them have disappeared.
+     *
      * @param enforceMonotonic Validate that incoming value is strictly
      *            monotonic compared to this object.
+     * @param clampNegative Instead of throwing like {@code enforceMonotonic},
+     *            clamp resulting counters at 0 to prevent negative values.
      */
-    public NetworkStats subtract(NetworkStats value, boolean enforceMonotonic) {
+    private NetworkStats subtract(
+            NetworkStats value, boolean enforceMonotonic, boolean clampNegative) {
         final long deltaRealtime = this.elapsedRealtime - value.elapsedRealtime;
         if (enforceMonotonic && deltaRealtime < 0) {
             throw new IllegalArgumentException("found non-monotonic realtime");
@@ -181,11 +208,15 @@
                 result.addEntry(iface, uid, this.rx[i], this.tx[i]);
             } else {
                 // existing row, subtract remote value
-                final long rx = this.rx[i] - value.rx[j];
-                final long tx = this.tx[i] - value.tx[j];
+                long rx = this.rx[i] - value.rx[j];
+                long tx = this.tx[i] - value.tx[j];
                 if (enforceMonotonic && (rx < 0 || tx < 0)) {
                     throw new IllegalArgumentException("found non-monotonic values");
                 }
+                if (clampNegative) {
+                    rx = Math.max(0, rx);
+                    tx = Math.max(0, tx);
+                }
                 result.addEntry(iface, uid, rx, tx);
             }
         }
diff --git a/core/java/android/net/NetworkStatsHistory.java b/core/java/android/net/NetworkStatsHistory.java
index 5edbf58..a697e96 100644
--- a/core/java/android/net/NetworkStatsHistory.java
+++ b/core/java/android/net/NetworkStatsHistory.java
@@ -218,7 +218,7 @@
      * Return interpolated data usage across the requested range. Interpolates
      * across buckets, so values may be rounded slightly.
      */
-    public void getTotalData(long start, long end, long[] outTotal) {
+    public long[] getTotalData(long start, long end, long[] outTotal) {
         long rx = 0;
         long tx = 0;
 
@@ -238,8 +238,12 @@
             }
         }
 
+        if (outTotal == null || outTotal.length != 2) {
+            outTotal = new long[2];
+        }
         outTotal[0] = rx;
         outTotal[1] = tx;
+        return outTotal;
     }
 
     /**
diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java
index a0738c1..8a688d5 100644
--- a/core/java/android/net/TrafficStats.java
+++ b/core/java/android/net/TrafficStats.java
@@ -177,8 +177,8 @@
 
             // subtract starting values and return delta
             final NetworkStats profilingStop = getNetworkStatsForUid(context);
-            final NetworkStats profilingDelta = profilingStop.subtract(
-                    sActiveProfilingStart, false);
+            final NetworkStats profilingDelta = profilingStop.subtractClamped(
+                    sActiveProfilingStart);
             sActiveProfilingStart = null;
             return profilingDelta;
         }
diff --git a/core/tests/coretests/src/android/net/NetworkStatsTest.java b/core/tests/coretests/src/android/net/NetworkStatsTest.java
index 23eb9cf..8a3e871 100644
--- a/core/tests/coretests/src/android/net/NetworkStatsTest.java
+++ b/core/tests/coretests/src/android/net/NetworkStatsTest.java
@@ -47,7 +47,7 @@
                 .addEntry(TEST_IFACE, 100, 1024, 0)
                 .addEntry(TEST_IFACE, 101, 0, 1024).build();
 
-        final NetworkStats result = after.subtract(before, true);
+        final NetworkStats result = after.subtract(before);
 
         // identical data should result in zero delta
         assertEquals(0, result.rx[0]);
@@ -65,7 +65,7 @@
                 .addEntry(TEST_IFACE, 100, 1025, 2)
                 .addEntry(TEST_IFACE, 101, 3, 1028).build();
 
-        final NetworkStats result = after.subtract(before, true);
+        final NetworkStats result = after.subtract(before);
 
         // expect delta between measurements
         assertEquals(1, result.rx[0]);
@@ -84,7 +84,7 @@
                 .addEntry(TEST_IFACE, 101, 0, 1024)
                 .addEntry(TEST_IFACE, 102, 1024, 1024).build();
 
-        final NetworkStats result = after.subtract(before, true);
+        final NetworkStats result = after.subtract(before);
 
         // its okay to have new rows
         assertEquals(0, result.rx[0]);
diff --git a/services/java/com/android/server/net/NetworkIdentity.java b/services/java/com/android/server/net/NetworkIdentity.java
index 79feb95..f7a7c49 100644
--- a/services/java/com/android/server/net/NetworkIdentity.java
+++ b/services/java/com/android/server/net/NetworkIdentity.java
@@ -66,7 +66,7 @@
             case VERSION_CURRENT: {
                 type = in.readInt();
                 subType = in.readInt();
-                subscriberId = in.readUTF();
+                subscriberId = readOptionalString(in);
                 break;
             }
             default: {
@@ -79,7 +79,7 @@
         out.writeInt(VERSION_CURRENT);
         out.writeInt(type);
         out.writeInt(subType);
-        out.writeUTF(subscriberId);
+        writeOptionalString(out, subscriberId);
     }
 
     @Override
@@ -205,4 +205,21 @@
         return new NetworkIdentity(type, subType, subscriberId);
     }
 
+    private static void writeOptionalString(DataOutputStream out, String value) throws IOException {
+        if (value != null) {
+            out.writeByte(1);
+            out.writeUTF(value);
+        } else {
+            out.writeByte(0);
+        }
+    }
+
+    private static String readOptionalString(DataInputStream in) throws IOException {
+        if (in.readByte() != 0) {
+            return in.readUTF();
+        } else {
+            return null;
+        }
+    }
+
 }
diff --git a/services/java/com/android/server/net/NetworkStatsService.java b/services/java/com/android/server/net/NetworkStatsService.java
index 3892de8..8db2839 100644
--- a/services/java/com/android/server/net/NetworkStatsService.java
+++ b/services/java/com/android/server/net/NetworkStatsService.java
@@ -48,6 +48,7 @@
 import android.net.NetworkState;
 import android.net.NetworkStats;
 import android.net.NetworkStatsHistory;
+import android.os.Environment;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.INetworkManagementService;
@@ -60,15 +61,26 @@
 import android.util.SparseArray;
 import android.util.TrustedTime;
 
+import com.android.internal.os.AtomicFile;
 import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
 import java.io.FileDescriptor;
+import java.io.FileInputStream;
+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.HashMap;
 import java.util.HashSet;
 
+import libcore.io.IoUtils;
+
 /**
  * Collect and persist detailed network statistics, and provide this data to
  * other system services.
@@ -77,6 +89,10 @@
     private static final String TAG = "NetworkStatsService";
     private static final boolean LOGD = true;
 
+    /** File header magic number: "ANET" */
+    private static final int FILE_MAGIC = 0x414E4554;
+    private static final int VERSION_CURRENT = 1;
+
     private final Context mContext;
     private final INetworkManagementService mNetworkManager;
     private final IAlarmManager mAlarmManager;
@@ -84,7 +100,8 @@
 
     private IConnectivityManager mConnManager;
 
-    private static final String ACTION_NETWORK_STATS_POLL =
+    // @VisibleForTesting
+    public static final String ACTION_NETWORK_STATS_POLL =
             "com.android.server.action.NETWORK_STATS_POLL";
 
     private PendingIntent mPollIntent;
@@ -98,14 +115,15 @@
     private LongSecureSetting mPollInterval = new LongSecureSetting(
             NETSTATS_POLL_INTERVAL, 15 * MINUTE_IN_MILLIS);
     private LongSecureSetting mPersistThreshold = new LongSecureSetting(
-            NETSTATS_PERSIST_THRESHOLD, 64 * KB_IN_BYTES);
+            NETSTATS_PERSIST_THRESHOLD, 16 * KB_IN_BYTES);
 
+    // TODO: adjust these timings for production builds
     private LongSecureSetting mSummaryBucketDuration = new LongSecureSetting(
-            NETSTATS_SUMMARY_BUCKET_DURATION, 6 * HOUR_IN_MILLIS);
+            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, 6 * HOUR_IN_MILLIS);
+            NETSTATS_DETAIL_BUCKET_DURATION, 2 * HOUR_IN_MILLIS);
     private LongSecureSetting mDetailMaxHistory = new LongSecureSetting(
             NETSTATS_DETAIL_MAX_HISTORY, 90 * DAY_IN_MILLIS);
 
@@ -129,6 +147,8 @@
     private final HandlerThread mHandlerThread;
     private final Handler mHandler;
 
+    private final AtomicFile mSummaryFile;
+
     // TODO: collect detailed uid stats, storing tag-granularity data until next
     // dropbox, and uid summary for a specific bucket count.
 
@@ -137,11 +157,15 @@
     public NetworkStatsService(
             Context context, INetworkManagementService networkManager, IAlarmManager alarmManager) {
         // TODO: move to using cached NtpTrustedTime
-        this(context, networkManager, alarmManager, new NtpTrustedTime());
+        this(context, networkManager, alarmManager, new NtpTrustedTime(), getSystemDir());
+    }
+
+    private static File getSystemDir() {
+        return new File(Environment.getDataDirectory(), "system");
     }
 
     public NetworkStatsService(Context context, INetworkManagementService networkManager,
-            IAlarmManager alarmManager, TrustedTime time) {
+            IAlarmManager alarmManager, TrustedTime time, File systemDir) {
         mContext = checkNotNull(context, "missing Context");
         mNetworkManager = checkNotNull(networkManager, "missing INetworkManagementService");
         mAlarmManager = checkNotNull(alarmManager, "missing IAlarmManager");
@@ -150,11 +174,15 @@
         mHandlerThread = new HandlerThread(TAG);
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
+
+        mSummaryFile = new AtomicFile(new File(systemDir, "netstats.bin"));
     }
 
     public void systemReady() {
-        // read historical stats from disk
-        readStatsLocked();
+        synchronized (mStatsLock) {
+            // read historical stats from disk
+            readStatsLocked();
+        }
 
         // watch other system services that claim interfaces
         final IntentFilter ifaceFilter = new IntentFilter();
@@ -180,6 +208,16 @@
         mConnManager = checkNotNull(connManager, "missing IConnectivityManager");
     }
 
+    private void shutdownLocked() {
+        mContext.unregisterReceiver(mIfaceReceiver);
+        mContext.unregisterReceiver(mPollReceiver);
+        mContext.unregisterReceiver(mShutdownReceiver);
+
+        writeStatsLocked();
+        mSummaryStats.clear();
+        mDetailStats.clear();
+    }
+
     /**
      * Clear any existing {@link #ACTION_NETWORK_STATS_POLL} alarms, and
      * reschedule based on current {@link #mPollInterval} value.
@@ -227,7 +265,7 @@
     }
 
     @Override
-    public NetworkStats getSummaryPerUid(long start, long end, int networkTemplate) {
+    public NetworkStats getSummaryForAllUid(long start, long end, int networkTemplate) {
         // TODO: create relaxed permission for reading stats
         mContext.enforceCallingOrSelfPermission(UPDATE_DEVICE_STATS, TAG);
 
@@ -281,7 +319,7 @@
         public void onReceive(Context context, Intent intent) {
             // verified SHUTDOWN permission above.
             synchronized (mStatsLock) {
-                writeStatsLocked();
+                shutdownLocked();
             }
         }
     };
@@ -440,13 +478,74 @@
 
     private void readStatsLocked() {
         if (LOGD) Slog.v(TAG, "readStatsLocked()");
-        // TODO: read historical stats from disk using AtomicFile
+
+        // clear any existing stats and read from disk
+        mSummaryStats.clear();
+
+        FileInputStream fis = null;
+        try {
+            fis = mSummaryFile.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 interfaces and stats:
+                    // summary := 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);
+                    }
+                    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 network stats", e);
+        } finally {
+            IoUtils.closeQuietly(fis);
+        }
     }
 
     private void writeStatsLocked() {
         if (LOGD) Slog.v(TAG, "writeStatsLocked()");
-        // TODO: persist historical stats to disk using AtomicFile
+
         // TODO: consider duplicating stats and releasing lock while writing
+
+        FileOutputStream fos = null;
+        try {
+            fos = mSummaryFile.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);
+                ident.writeToStream(out);
+                history.writeToStream(out);
+            }
+
+            mSummaryFile.finishWrite(fos);
+        } catch (IOException e) {
+            if (fos != null) {
+                mSummaryFile.failWrite(fos);
+            }
+        }
     }
 
     @Override
@@ -466,6 +565,12 @@
                 return;
             }
 
+            if (argSet.contains("poll")) {
+                performPollLocked();
+                pw.println("Forced poll");
+                return;
+            }
+
             pw.println("Active interfaces:");
             for (String iface : mActiveIface.keySet()) {
                 final InterfaceIdentity ident = mActiveIface.get(iface);
@@ -545,7 +650,7 @@
      */
     private static NetworkStats computeStatsDelta(NetworkStats before, NetworkStats current) {
         if (before != null) {
-            return current.subtract(before, false);
+            return current.subtractClamped(before);
         } else {
             return current;
         }
diff --git a/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java
new file mode 100644
index 0000000..9846372
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+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 com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static org.easymock.EasyMock.anyLong;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.isA;
+
+import android.app.AlarmManager;
+import android.app.IAlarmManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.net.IConnectivityManager;
+import android.net.LinkProperties;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkState;
+import android.net.NetworkStats;
+import android.net.NetworkStatsHistory;
+import android.os.INetworkManagementService;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.TrustedTime;
+
+import com.android.server.net.NetworkStatsService;
+
+import org.easymock.EasyMock;
+
+import java.io.File;
+
+/**
+ * Tests for {@link NetworkStatsService}.
+ */
+@LargeTest
+public class NetworkStatsServiceTest extends AndroidTestCase {
+    private static final String TAG = "NetworkStatsServiceTest";
+
+    private static final String TEST_IFACE = "test0";
+    private static final long TEST_START = 1194220800000L;
+
+    private BroadcastInterceptingContext mServiceContext;
+    private File mStatsDir;
+
+    private INetworkManagementService mNetManager;
+    private IAlarmManager mAlarmManager;
+    private TrustedTime mTime;
+    private IConnectivityManager mConnManager;
+
+    private NetworkStatsService mService;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mServiceContext = new BroadcastInterceptingContext(getContext());
+        mStatsDir = getContext().getFilesDir();
+
+        mNetManager = createMock(INetworkManagementService.class);
+        mAlarmManager = createMock(IAlarmManager.class);
+        mTime = createMock(TrustedTime.class);
+        mConnManager = createMock(IConnectivityManager.class);
+
+        mService = new NetworkStatsService(
+                mServiceContext, mNetManager, mAlarmManager, mTime, mStatsDir);
+        mService.bindConnectivityManager(mConnManager);
+
+        expectSystemReady();
+
+        replay();
+        mService.systemReady();
+        verifyAndReset();
+
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        for (File file : mStatsDir.listFiles()) {
+            file.delete();
+        }
+
+        mServiceContext = null;
+        mStatsDir = null;
+
+        mNetManager = null;
+        mAlarmManager = null;
+        mTime = null;
+
+        mService = null;
+
+        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 {
+        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);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION));
+        verifyAndReset();
+
+        // verify service has empty history for wifi
+        assertNetworkTotal(TEMPLATE_WIFI, 0L, 0L);
+
+        // 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);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+        verifyAndReset();
+
+        // verify service recorded history
+        assertNetworkTotal(TEMPLATE_WIFI, 1024L, 2048L);
+
+        // 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);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+        verifyAndReset();
+
+        // verify service recorded history
+        assertNetworkTotal(TEMPLATE_WIFI, 4096L, 8192L);
+    }
+
+    public void testHistoryForRebootPersist() 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());
+
+        // 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);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION));
+        verifyAndReset();
+
+        // verify service has empty history for wifi
+        assertNetworkTotal(TEMPLATE_WIFI, 0L, 0L);
+
+        // 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);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+        verifyAndReset();
+
+        // verify service recorded history
+        assertNetworkTotal(TEMPLATE_WIFI, 1024L, 2048L);
+
+        // graceful shutdown system, which should trigger persist of stats, and
+        // clear any values in memory.
+        mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+
+        // talk with zombie service to assert stats have gone; and assert that
+        // we persisted them to file.
+        assertNetworkTotal(TEMPLATE_WIFI, 0L, 0L);
+        assertTrue(statsFile.exists());
+
+        // boot through serviceReady() again
+        expectSystemReady();
+
+        replay();
+        mService.systemReady();
+        verifyAndReset();
+
+        // after systemReady(), we should have historical stats loaded again
+        assertNetworkTotal(TEMPLATE_WIFI, 1024L, 2048L);
+
+    }
+
+    private void assertNetworkTotal(int template, long rx, long tx) {
+        final NetworkStatsHistory history = mService.getHistoryForNetwork(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();
+
+        mAlarmManager.setInexactRepeating(
+                eq(AlarmManager.ELAPSED_REALTIME), anyLong(), anyLong(), isA(PendingIntent.class));
+        expectLastCall().atLeastOnce();
+    }
+
+    public void expectTime(long currentTime) throws Exception {
+        expect(mTime.forceRefresh()).andReturn(false).anyTimes();
+        expect(mTime.hasCache()).andReturn(true).anyTimes();
+        expect(mTime.currentTimeMillis()).andReturn(currentTime).anyTimes();
+        expect(mTime.getCacheAge()).andReturn(0L).anyTimes();
+        expect(mTime.getCacheCertainty()).andReturn(0L).anyTimes();
+    }
+
+    private void replay() {
+        EasyMock.replay(mNetManager, mAlarmManager, mTime, mConnManager);
+    }
+
+    private void verifyAndReset() {
+        EasyMock.verify(mNetManager, mAlarmManager, mTime, mConnManager);
+        EasyMock.reset(mNetManager, mAlarmManager, mTime, mConnManager);
+    }
+}