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);
+ }
+}