Interface-level network policy, persist policies.

Define NetworkPolicy as cycle-reset day and warning/limit values, and
set/get through NetworkPolicyManager.  Watch ConnectivityManager for
network connection events, and apply quota rules based on matching
interfaces.  Policy service matches based on strong identity to support
IMSI-specific policy values.

Calculates remaining quota based on current stats recorded since the
last reset cycle day.  Tests to verify edge cases around February.

Persist network and UID policies in XML, and restore on boot.

Change-Id: Id40ba7d6eed6094fbd5e18e6331286c606880d80
diff --git a/core/java/android/net/INetworkPolicyManager.aidl b/core/java/android/net/INetworkPolicyManager.aidl
index c1f3530..c9238eb 100644
--- a/core/java/android/net/INetworkPolicyManager.aidl
+++ b/core/java/android/net/INetworkPolicyManager.aidl
@@ -17,6 +17,7 @@
 package android.net;
 
 import android.net.INetworkPolicyListener;
+import android.net.NetworkPolicy;
 
 /**
  * Interface that creates and modifies network policy rules.
@@ -33,6 +34,7 @@
     void registerListener(INetworkPolicyListener listener);
     void unregisterListener(INetworkPolicyListener listener);
 
-    // TODO: build API to surface stats details for settings UI
+    void setNetworkPolicy(int networkType, String subscriberId, in NetworkPolicy policy);
+    NetworkPolicy getNetworkPolicy(int networkType, String subscriberId);
 
 }
diff --git a/core/java/android/net/INetworkStatsService.aidl b/core/java/android/net/INetworkStatsService.aidl
index d05c9d3..288112a 100644
--- a/core/java/android/net/INetworkStatsService.aidl
+++ b/core/java/android/net/INetworkStatsService.aidl
@@ -27,6 +27,8 @@
     /** Return historical stats for specific UID traffic that matches template. */
     NetworkStatsHistory getHistoryForUid(int uid, int networkTemplate);
 
+    /** Return usage summary for traffic that matches template. */
+    NetworkStats getSummaryForNetwork(long start, long end, int networkTemplate, String subscriberId);
     /** Return usage summary per UID for traffic that matches template. */
     NetworkStats getSummaryForAllUid(long start, long end, int networkTemplate);
 
diff --git a/core/java/android/net/NetworkPolicy.aidl b/core/java/android/net/NetworkPolicy.aidl
new file mode 100644
index 0000000..dbabb0608
--- /dev/null
+++ b/core/java/android/net/NetworkPolicy.aidl
@@ -0,0 +1,19 @@
+/**
+ * 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 android.net;
+
+parcelable NetworkPolicy;
diff --git a/core/java/android/net/NetworkPolicy.java b/core/java/android/net/NetworkPolicy.java
new file mode 100644
index 0000000..b9909c3
--- /dev/null
+++ b/core/java/android/net/NetworkPolicy.java
@@ -0,0 +1,81 @@
+/*
+ * 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 android.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Policy for a specific network, including usage cycle and limits to be
+ * enforced.
+ *
+ * @hide
+ */
+public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
+    public final int cycleDay;
+    public final long warningBytes;
+    public final long limitBytes;
+
+    public NetworkPolicy(int cycleDay, long warningBytes, long limitBytes) {
+        this.cycleDay = cycleDay;
+        this.warningBytes = warningBytes;
+        this.limitBytes = limitBytes;
+    }
+
+    public NetworkPolicy(Parcel in) {
+        cycleDay = in.readInt();
+        warningBytes = in.readLong();
+        limitBytes = in.readLong();
+    }
+
+    /** {@inheritDoc} */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(cycleDay);
+        dest.writeLong(warningBytes);
+        dest.writeLong(limitBytes);
+    }
+
+    /** {@inheritDoc} */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    public int compareTo(NetworkPolicy another) {
+        if (another == null || limitBytes < another.limitBytes) {
+            return -1;
+        } else {
+            return 1;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "NetworkPolicy: cycleDay=" + cycleDay + ", warningBytes=" + warningBytes
+                + ", limitBytes=" + limitBytes;
+    }
+
+    public static final Creator<NetworkPolicy> CREATOR = new Creator<NetworkPolicy>() {
+        public NetworkPolicy createFromParcel(Parcel in) {
+            return new NetworkPolicy(in);
+        }
+
+        public NetworkPolicy[] newArray(int size) {
+            return new NetworkPolicy[size];
+        }
+    };
+}
diff --git a/core/java/android/net/NetworkPolicyManager.java b/core/java/android/net/NetworkPolicyManager.java
index dd7c1b0..0f4dc9a 100644
--- a/core/java/android/net/NetworkPolicyManager.java
+++ b/core/java/android/net/NetworkPolicyManager.java
@@ -51,6 +51,23 @@
         return (NetworkPolicyManager) context.getSystemService(Context.NETWORK_POLICY_SERVICE);
     }
 
+    /** {@hide} */
+    public void setNetworkPolicy(int networkType, String subscriberId, NetworkPolicy policy) {
+        try {
+            mService.setNetworkPolicy(networkType, subscriberId, policy);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /** {@hide} */
+    public NetworkPolicy getNetworkPolicy(int networkType, String subscriberId) {
+        try {
+            return mService.getNetworkPolicy(networkType, subscriberId);
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
     /**
      * Set policy flags for specific UID.
      *
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b2606c1..a8aff37 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1377,6 +1377,19 @@
     <permission android:name="android.permission.CRYPT_KEEPER"
         android:protectionLevel="signatureOrSystem" />
 
+    <!-- Allows an application to read historical network usage for
+         specific networks and applications. @hide -->
+    <permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY"
+        android:label="@string/permlab_readNetworkUsageHistory"
+        android:description="@string/permdesc_readNetworkUsageHistory"
+        android:protectionLevel="signature" />
+
+    <!-- Allows an application to manage network policies (such as warning and disable
+         limits) and to define application-specific rules. @hide -->
+    <permission android:name="android.permission.MANAGE_NETWORK_POLICY"
+        android:label="@string/permlab_manageNetworkPolicy"
+        android:description="@string/permdesc_manageNetworkPolicy"
+        android:protectionLevel="signature" />
 
     <!-- C2DM permission. 
          @hide Used internally.
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 3736157..b264b41 100755
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1431,6 +1431,16 @@
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permdesc_use_sip">Allows an application to use the SIP service to make/receive Internet calls.</string>
 
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_readNetworkUsageHistory">read historical network usage</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_readNetworkUsageHistory">Allows an application to read historical network usage for specific networks and applications.</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_manageNetworkPolicy">manage network policy</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_manageNetworkPolicy">Allows an application to manage network policies and define application-specific rules.</string>
+
     <!-- Policy administration -->
 
     <!-- Title of policy access to limiting the user's password choices -->
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index fd03e5f..3484baf 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -306,6 +306,7 @@
                 connectivity = new ConnectivityService(context, networkManagement, networkPolicy);
                 ServiceManager.addService(Context.CONNECTIVITY_SERVICE, connectivity);
                 networkStats.bindConnectivityManager(connectivity);
+                networkPolicy.bindConnectivityManager(connectivity);
             } catch (Throwable e) {
                 Slog.e(TAG, "Failure starting Connectivity Service", e);
             }
diff --git a/services/java/com/android/server/net/NetworkPolicyManagerService.java b/services/java/com/android/server/net/NetworkPolicyManagerService.java
index 1a90a92..c8f617e 100644
--- a/services/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -16,15 +16,22 @@
 
 package com.android.server.net;
 
+import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
 import static android.Manifest.permission.DUMP;
 import static android.Manifest.permission.MANAGE_APP_TOKENS;
-import static android.Manifest.permission.UPDATE_DEVICE_STATS;
+import static android.Manifest.permission.MANAGE_NETWORK_POLICY;
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.NetworkPolicyManager.POLICY_NONE;
 import static android.net.NetworkPolicyManager.POLICY_REJECT_PAID_BACKGROUND;
 import static android.net.NetworkPolicyManager.RULE_ALLOW_ALL;
 import static android.net.NetworkPolicyManager.RULE_REJECT_PAID;
 import static android.net.NetworkPolicyManager.dumpPolicy;
 import static android.net.NetworkPolicyManager.dumpRules;
+import static android.text.format.DateUtils.DAY_IN_MILLIS;
+import static android.text.format.Time.MONTH_DAY;
+import static com.android.internal.util.Preconditions.checkNotNull;
+import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
+import static org.xmlpull.v1.XmlPullParser.START_TAG;
 
 import android.app.IActivityManager;
 import android.app.IProcessObserver;
@@ -33,19 +40,51 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.net.ConnectivityManager;
+import android.net.IConnectivityManager;
 import android.net.INetworkPolicyListener;
 import android.net.INetworkPolicyManager;
 import android.net.INetworkStatsService;
+import android.net.NetworkPolicy;
+import android.net.NetworkState;
+import android.net.NetworkStats;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IPowerManager;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
+import android.text.format.Time;
+import android.util.NtpTrustedTime;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
+import android.util.TrustedTime;
+import android.util.Xml;
 
+import com.android.internal.os.AtomicFile;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.Objects;
+import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+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.Arrays;
+import java.util.HashMap;
+
+import libcore.io.IoUtils;
 
 /**
  * Service that maintains low-level network policy rules and collects usage
@@ -59,10 +98,30 @@
     private static final String TAG = "NetworkPolicy";
     private static final boolean LOGD = true;
 
+    private static final int VERSION_CURRENT = 1;
+
+    private static final String TAG_POLICY_LIST = "policy-list";
+    private static final String TAG_NETWORK_POLICY = "network-policy";
+    private static final String TAG_UID_POLICY = "uid-policy";
+
+    private static final String ATTR_VERSION = "version";
+    private static final String ATTR_NETWORK_TEMPLATE = "networkTemplate";
+    private static final String ATTR_SUBSCRIBER_ID = "subscriberId";
+    private static final String ATTR_CYCLE_DAY = "cycleDay";
+    private static final String ATTR_WARNING_BYTES = "warningBytes";
+    private static final String ATTR_LIMIT_BYTES = "limitBytes";
+    private static final String ATTR_UID = "uid";
+    private static final String ATTR_POLICY = "policy";
+
+    private static final long TIME_CACHE_MAX_AGE = DAY_IN_MILLIS;
+
     private final Context mContext;
     private final IActivityManager mActivityManager;
     private final IPowerManager mPowerManager;
     private final INetworkStatsService mNetworkStats;
+    private final TrustedTime mTime;
+
+    private IConnectivityManager mConnManager;
 
     private final Object mRulesLock = new Object();
 
@@ -73,6 +132,9 @@
     /** Current derived network rules for each UID. */
     private SparseIntArray mUidRules = new SparseIntArray();
 
+    /** Set of policies for strong network templates. */
+    private HashMap<StrongTemplate, NetworkPolicy> mTemplatePolicy = Maps.newHashMap();
+
     /** Foreground at both UID and PID granularity. */
     private SparseBooleanArray mUidForeground = new SparseBooleanArray();
     private SparseArray<SparseBooleanArray> mUidPidForeground = new SparseArray<
@@ -81,26 +143,52 @@
     private final RemoteCallbackList<INetworkPolicyListener> mListeners = new RemoteCallbackList<
             INetworkPolicyListener>();
 
-    // TODO: save/restore policy information from disk
+    private final HandlerThread mHandlerThread;
+    private final Handler mHandler;
+
+    private final AtomicFile mPolicyFile;
 
     // TODO: keep whitelist of system-critical services that should never have
     // rules enforced, such as system, phone, and radio UIDs.
 
-    // TODO: keep record of billing cycle details, and limit rules
-    // TODO: keep map of interfaces-to-billing-relationship
-
     // TODO: dispatch callbacks through handler when locked
 
     public NetworkPolicyManagerService(Context context, IActivityManager activityManager,
             IPowerManager powerManager, INetworkStatsService networkStats) {
+        // TODO: move to using cached NtpTrustedTime
+        this(context, activityManager, powerManager, networkStats, new NtpTrustedTime(),
+                getSystemDir());
+    }
+
+    private static File getSystemDir() {
+        return new File(Environment.getDataDirectory(), "system");
+    }
+
+    public NetworkPolicyManagerService(Context context, IActivityManager activityManager,
+            IPowerManager powerManager, INetworkStatsService networkStats, TrustedTime time,
+            File systemDir) {
         mContext = checkNotNull(context, "missing context");
         mActivityManager = checkNotNull(activityManager, "missing activityManager");
         mPowerManager = checkNotNull(powerManager, "missing powerManager");
         mNetworkStats = checkNotNull(networkStats, "missing networkStats");
+        mTime = checkNotNull(time, "missing TrustedTime");
+
+        mHandlerThread = new HandlerThread(TAG);
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+
+        mPolicyFile = new AtomicFile(new File(systemDir, "netpolicy.xml"));
+    }
+
+    public void bindConnectivityManager(IConnectivityManager connManager) {
+        mConnManager = checkNotNull(connManager, "missing IConnectivityManager");
     }
 
     public void systemReady() {
-        // TODO: read current policy from disk
+        synchronized (mRulesLock) {
+            // read policy from disk
+            readPolicyLocked();
+        }
 
         updateScreenOn();
 
@@ -120,6 +208,11 @@
         screenFilter.addAction(Intent.ACTION_SCREEN_OFF);
         mContext.registerReceiver(mScreenReceiver, screenFilter);
 
+        // watch for network interfaces to be claimed
+        final IntentFilter ifaceFilter = new IntentFilter();
+        ifaceFilter.addAction(CONNECTIVITY_ACTION);
+        mContext.registerReceiver(mIfaceReceiver, ifaceFilter, CONNECTIVITY_INTERNAL, mHandler);
+
     }
 
     private IProcessObserver mProcessObserver = new IProcessObserver.Stub() {
@@ -139,7 +232,7 @@
                     mUidPidForeground.put(uid, pidForeground);
                 }
                 pidForeground.put(pid, foregroundActivities);
-                computeUidForegroundL(uid);
+                computeUidForegroundLocked(uid);
             }
         }
 
@@ -153,7 +246,7 @@
                 final SparseBooleanArray pidForeground = mUidPidForeground.get(uid);
                 if (pidForeground != null) {
                     pidForeground.delete(pid);
-                    computeUidForegroundL(uid);
+                    computeUidForegroundLocked(uid);
                 }
             }
         }
@@ -170,23 +263,279 @@
         }
     };
 
+    /**
+     * Receiver that watches for {@link IConnectivityManager} to claim network
+     * interfaces. Used to apply {@link NetworkPolicy} when networks match
+     * {@link StrongTemplate}.
+     */
+    private BroadcastReceiver mIfaceReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // on background handler thread, and verified CONNECTIVITY_INTERNAL
+            // permission above.
+            synchronized (mRulesLock) {
+                updateIfacesLocked();
+            }
+        }
+    };
+
+    /**
+     * Examine all connected {@link NetworkState}, looking for
+     * {@link NetworkPolicy} that need to be enforced. When matches found, set
+     * remaining quota based on usage cycle and historical stats.
+     */
+    private void updateIfacesLocked() {
+        if (LOGD) Slog.v(TAG, "updateIfacesLocked()");
+
+        final NetworkState[] states;
+        try {
+            states = mConnManager.getAllNetworkState();
+        } catch (RemoteException e) {
+            Slog.w(TAG, "problem reading network state");
+            return;
+        }
+
+        // first, derive identity for all connected networks, which can be used
+        // to match against templates.
+        final HashMap<NetworkIdentity, String> networks = Maps.newHashMap();
+        for (NetworkState state : states) {
+            // stash identity and iface away for later use
+            if (state.networkInfo.isConnected()) {
+                final String iface = state.linkProperties.getInterfaceName();
+                final NetworkIdentity ident = NetworkIdentity.buildNetworkIdentity(mContext, state);
+                networks.put(ident, iface);
+            }
+        }
+
+        // build list of rules and ifaces to enforce them against
+        final HashMap<StrongTemplate, String[]> rules = Maps.newHashMap();
+        final ArrayList<String> ifaceList = Lists.newArrayList();
+        for (StrongTemplate template : mTemplatePolicy.keySet()) {
+
+            // collect all active ifaces that match this template
+            ifaceList.clear();
+            for (NetworkIdentity ident : networks.keySet()) {
+                if (ident.matchesTemplate(template.networkTemplate, template.subscriberId)) {
+                    final String iface = networks.get(ident);
+                    ifaceList.add(iface);
+                }
+            }
+
+            if (ifaceList.size() > 0) {
+                final String[] ifaces = ifaceList.toArray(new String[ifaceList.size()]);
+                rules.put(template, ifaces);
+            }
+        }
+
+        // try refreshing time source when stale
+        if (mTime.getCacheAge() > TIME_CACHE_MAX_AGE) {
+            mTime.forceRefresh();
+        }
+
+        final long currentTime = mTime.hasCache() ? mTime.currentTimeMillis()
+                : System.currentTimeMillis();
+
+        // apply each policy that we found ifaces for; compute remaining data
+        // based on current cycle and historical stats, and push to kernel.
+        for (StrongTemplate template : rules.keySet()) {
+            final NetworkPolicy policy = mTemplatePolicy.get(template);
+            final String[] ifaces = rules.get(policy);
+
+            final long start = computeLastCycleBoundary(currentTime, policy);
+            final long end = currentTime;
+
+            final NetworkStats stats;
+            try {
+                stats = mNetworkStats.getSummaryForNetwork(
+                        start, end, template.networkTemplate, template.subscriberId);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "problem reading summary for template " + template.networkTemplate);
+                continue;
+            }
+
+            // remaining "quota" is based on usage in current cycle
+            final long total = stats.rx[0] + stats.tx[0];
+            final long quota = Math.max(0, policy.limitBytes - total);
+
+            if (LOGD) {
+                Slog.d(TAG, "applying policy " + policy.toString() + " to ifaces "
+                        + Arrays.toString(ifaces) + " with quota " + quota);
+            }
+
+            // TODO: push rule down through NetworkManagementService.setInterfaceQuota()
+
+        }
+    }
+
+    /**
+     * Compute the last cycle boundary for the given {@link NetworkPolicy}. For
+     * example, if cycle day is 20th, and today is June 15th, it will return May
+     * 20th. When cycle day doesn't exist in current month, it snaps to the 1st
+     * of following month.
+     */
+    // @VisibleForTesting
+    public static long computeLastCycleBoundary(long currentTime, NetworkPolicy policy) {
+        final Time now = new Time(Time.TIMEZONE_UTC);
+        now.set(currentTime);
+
+        // first, find cycle boundary for current month
+        final Time cycle = new Time(now);
+        cycle.hour = cycle.minute = cycle.second = 0;
+        snapToCycleDay(cycle, policy.cycleDay);
+
+        if (cycle.after(now)) {
+            // cycle boundary is beyond now, use last cycle boundary; start by
+            // pushing ourselves squarely into last month.
+            final Time lastMonth = new Time(now);
+            lastMonth.hour = lastMonth.minute = lastMonth.second = 0;
+            lastMonth.monthDay = 1;
+            lastMonth.month -= 1;
+            lastMonth.normalize(true);
+
+            cycle.set(lastMonth);
+            snapToCycleDay(cycle, policy.cycleDay);
+        }
+
+        return cycle.toMillis(true);
+    }
+
+    /**
+     * Snap to the cycle day for the current month given; when cycle day doesn't
+     * exist, it snaps to 1st of following month.
+     */
+    private static void snapToCycleDay(Time time, int cycleDay) {
+        if (cycleDay > time.getActualMaximum(MONTH_DAY)) {
+            // cycle day isn't valid this month; snap to 1st of next month
+            time.month += 1;
+            time.monthDay = 1;
+        } else {
+            time.monthDay = cycleDay;
+        }
+        time.normalize(true);
+    }
+
+    private void readPolicyLocked() {
+        if (LOGD) Slog.v(TAG, "readPolicyLocked()");
+
+        // clear any existing policy and read from disk
+        mTemplatePolicy.clear();
+        mUidPolicy.clear();
+
+        FileInputStream fis = null;
+        try {
+            fis = mPolicyFile.openRead();
+            final XmlPullParser in = Xml.newPullParser();
+            in.setInput(fis, null);
+
+            int type;
+            int version = VERSION_CURRENT;
+            while ((type = in.next()) != END_DOCUMENT) {
+                final String tag = in.getName();
+                if (type == START_TAG) {
+                    if (TAG_POLICY_LIST.equals(tag)) {
+                        version = readIntAttribute(in, ATTR_VERSION);
+
+                    } else if (TAG_NETWORK_POLICY.equals(tag)) {
+                        final int networkTemplate = readIntAttribute(in, ATTR_NETWORK_TEMPLATE);
+                        final String subscriberId = in.getAttributeValue(null, ATTR_SUBSCRIBER_ID);
+
+                        final int cycleDay = readIntAttribute(in, ATTR_CYCLE_DAY);
+                        final long warningBytes = readLongAttribute(in, ATTR_WARNING_BYTES);
+                        final long limitBytes = readLongAttribute(in, ATTR_LIMIT_BYTES);
+
+                        mTemplatePolicy.put(new StrongTemplate(networkTemplate, subscriberId),
+                                new NetworkPolicy(cycleDay, warningBytes, limitBytes));
+
+                    } else if (TAG_UID_POLICY.equals(tag)) {
+                        final int uid = readIntAttribute(in, ATTR_UID);
+                        final int policy = readIntAttribute(in, ATTR_POLICY);
+
+                        mUidPolicy.put(uid, policy);
+                    }
+                }
+            }
+
+        } catch (FileNotFoundException e) {
+            // missing policy is okay, probably first boot
+        } catch (IOException e) {
+            Slog.e(TAG, "problem reading network stats", e);
+        } catch (XmlPullParserException e) {
+            Slog.e(TAG, "problem reading network stats", e);
+        } finally {
+            IoUtils.closeQuietly(fis);
+        }
+    }
+
+    private void writePolicyLocked() {
+        if (LOGD) Slog.v(TAG, "writePolicyLocked()");
+
+        FileOutputStream fos = null;
+        try {
+            fos = mPolicyFile.startWrite();
+
+            XmlSerializer out = new FastXmlSerializer();
+            out.setOutput(fos, "utf-8");
+            out.startDocument(null, true);
+
+            out.startTag(null, TAG_POLICY_LIST);
+            writeIntAttribute(out, ATTR_VERSION, VERSION_CURRENT);
+
+            // write all known network policies
+            for (StrongTemplate template : mTemplatePolicy.keySet()) {
+                final NetworkPolicy policy = mTemplatePolicy.get(template);
+
+                out.startTag(null, TAG_NETWORK_POLICY);
+                writeIntAttribute(out, ATTR_NETWORK_TEMPLATE, template.networkTemplate);
+                if (template.subscriberId != null) {
+                    out.attribute(null, ATTR_SUBSCRIBER_ID, template.subscriberId);
+                }
+                writeIntAttribute(out, ATTR_CYCLE_DAY, policy.cycleDay);
+                writeLongAttribute(out, ATTR_WARNING_BYTES, policy.warningBytes);
+                writeLongAttribute(out, ATTR_LIMIT_BYTES, policy.limitBytes);
+                out.endTag(null, TAG_NETWORK_POLICY);
+            }
+
+            // write all known uid policies
+            for (int i = 0; i < mUidPolicy.size(); i++) {
+                final int uid = mUidPolicy.keyAt(i);
+                final int policy = mUidPolicy.valueAt(i);
+
+                out.startTag(null, TAG_UID_POLICY);
+                writeIntAttribute(out, ATTR_UID, uid);
+                writeIntAttribute(out, ATTR_POLICY, policy);
+                out.endTag(null, TAG_UID_POLICY);
+            }
+
+            out.endTag(null, TAG_POLICY_LIST);
+            out.endDocument();
+
+            mPolicyFile.finishWrite(fos);
+        } catch (IOException e) {
+            if (fos != null) {
+                mPolicyFile.failWrite(fos);
+            }
+        }
+    }
+
     @Override
     public void setUidPolicy(int uid, int policy) {
-        // TODO: create permission for modifying data policy
-        mContext.enforceCallingOrSelfPermission(UPDATE_DEVICE_STATS, TAG);
+        mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
 
         final int oldPolicy;
         synchronized (mRulesLock) {
             oldPolicy = getUidPolicy(uid);
             mUidPolicy.put(uid, policy);
-            updateRulesForUidL(uid);
-        }
 
-        // TODO: consider dispatching BACKGROUND_DATA_SETTING broadcast
+            // uid policy changed, recompute rules and persist policy.
+            updateRulesForUidLocked(uid);
+            writePolicyLocked();
+        }
     }
 
     @Override
     public int getUidPolicy(int uid) {
+        mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
+
         synchronized (mRulesLock) {
             return mUidPolicy.get(uid, POLICY_NONE);
         }
@@ -218,10 +567,40 @@
     }
 
     @Override
+    public void setNetworkPolicy(int networkType, String subscriberId, NetworkPolicy policy) {
+        mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
+
+        synchronized (mRulesLock) {
+            mTemplatePolicy.put(new StrongTemplate(networkType, subscriberId), policy);
+
+            // network policy changed, recompute template rules based on active
+            // interfaces and persist policy.
+            updateIfacesLocked();
+            writePolicyLocked();
+        }
+    }
+
+    @Override
+    public NetworkPolicy getNetworkPolicy(int networkType, String subscriberId) {
+        mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
+
+        synchronized (mRulesLock) {
+            return mTemplatePolicy.get(new StrongTemplate(networkType, subscriberId));
+        }
+    }
+
+    @Override
     protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
         mContext.enforceCallingOrSelfPermission(DUMP, TAG);
 
         synchronized (mRulesLock) {
+            fout.println("Network policies:");
+            for (StrongTemplate template : mTemplatePolicy.keySet()) {
+                final NetworkPolicy policy = mTemplatePolicy.get(template);
+                fout.print("  "); fout.println(template.toString());
+                fout.print("    "); fout.println(policy.toString());
+            }
+
             fout.println("Policy status for known UIDs:");
 
             final SparseBooleanArray knownUids = new SparseBooleanArray();
@@ -274,9 +653,9 @@
 
     /**
      * Foreground for PID changed; recompute foreground at UID level. If
-     * changed, will trigger {@link #updateRulesForUidL(int)}.
+     * changed, will trigger {@link #updateRulesForUidLocked(int)}.
      */
-    private void computeUidForegroundL(int uid) {
+    private void computeUidForegroundLocked(int uid) {
         final SparseBooleanArray pidForeground = mUidPidForeground.get(uid);
 
         // current pid is dropping foreground; examine other pids
@@ -293,7 +672,7 @@
         if (oldUidForeground != uidForeground) {
             // foreground changed, push updated rules
             mUidForeground.put(uid, uidForeground);
-            updateRulesForUidL(uid);
+            updateRulesForUidLocked(uid);
         }
     }
 
@@ -303,25 +682,25 @@
                 mScreenOn = mPowerManager.isScreenOn();
             } catch (RemoteException e) {
             }
-            updateRulesForScreenL();
+            updateRulesForScreenLocked();
         }
     }
 
     /**
      * Update rules that might be changed by {@link #mScreenOn} value.
      */
-    private void updateRulesForScreenL() {
+    private void updateRulesForScreenLocked() {
         // only update rules for anyone with foreground activities
         final int size = mUidForeground.size();
         for (int i = 0; i < size; i++) {
             if (mUidForeground.valueAt(i)) {
                 final int uid = mUidForeground.keyAt(i);
-                updateRulesForUidL(uid);
+                updateRulesForUidLocked(uid);
             }
         }
     }
 
-    private void updateRulesForUidL(int uid) {
+    private void updateRulesForUidLocked(int uid) {
         final int uidPolicy = getUidPolicy(uid);
         final boolean uidForeground = isUidForeground(uid);
 
@@ -351,13 +730,6 @@
         mListeners.finishBroadcast();
     }
 
-    private static <T> T checkNotNull(T value, String message) {
-        if (value == null) {
-            throw new NullPointerException(message);
-        }
-        return value;
-    }
-
     private static void collectKeys(SparseIntArray source, SparseBooleanArray target) {
         final int size = source.size();
         for (int i = 0; i < size; i++) {
@@ -381,4 +753,69 @@
         }
         fout.print("]");
     }
+
+    private static int readIntAttribute(XmlPullParser in, String name) throws IOException {
+        final String value = in.getAttributeValue(null, name);
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+            throw new ProtocolException("problem parsing " + name + "=" + value + " as int");
+        }
+    }
+
+    private static long readLongAttribute(XmlPullParser in, String name) throws IOException {
+        final String value = in.getAttributeValue(null, name);
+        try {
+            return Long.parseLong(value);
+        } catch (NumberFormatException e) {
+            throw new ProtocolException("problem parsing " + name + "=" + value + " as int");
+        }
+    }
+
+    private static void writeIntAttribute(XmlSerializer out, String name, int value)
+            throws IOException {
+        out.attribute(null, name, Integer.toString(value));
+    }
+
+    private static void writeLongAttribute(XmlSerializer out, String name, long value)
+            throws IOException {
+        out.attribute(null, name, Long.toString(value));
+    }
+
+    /**
+     * Network template with strong subscriber ID, used as key when defining
+     * {@link NetworkPolicy}.
+     */
+    private static class StrongTemplate {
+        public final int networkTemplate;
+        public final String subscriberId;
+
+        public StrongTemplate(int networkTemplate, String subscriberId) {
+            this.networkTemplate = networkTemplate;
+            this.subscriberId = subscriberId;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(networkTemplate, subscriberId);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof StrongTemplate) {
+                final StrongTemplate template = (StrongTemplate) obj;
+                return template.networkTemplate == networkTemplate
+                        && Objects.equal(template.subscriberId, subscriberId);
+            }
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            return "TemplateIdentity: networkTemplate=" + networkTemplate + ", subscriberId="
+                    + subscriberId;
+        }
+
+    }
+
 }
diff --git a/services/java/com/android/server/net/NetworkStatsService.java b/services/java/com/android/server/net/NetworkStatsService.java
index 8db2839..161c393 100644
--- a/services/java/com/android/server/net/NetworkStatsService.java
+++ b/services/java/com/android/server/net/NetworkStatsService.java
@@ -18,8 +18,8 @@
 
 import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
 import static android.Manifest.permission.DUMP;
+import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
 import static android.Manifest.permission.SHUTDOWN;
-import static android.Manifest.permission.UPDATE_DEVICE_STATS;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.NetworkStats.IFACE_ALL;
 import static android.net.NetworkStats.UID_ALL;
@@ -86,7 +86,7 @@
  * other system services.
  */
 public class NetworkStatsService extends INetworkStatsService.Stub {
-    private static final String TAG = "NetworkStatsService";
+    private static final String TAG = "NetworkStats";
     private static final boolean LOGD = true;
 
     /** File header magic number: "ANET" */
@@ -178,20 +178,25 @@
         mSummaryFile = new AtomicFile(new File(systemDir, "netstats.bin"));
     }
 
+    public void bindConnectivityManager(IConnectivityManager connManager) {
+        mConnManager = checkNotNull(connManager, "missing IConnectivityManager");
+    }
+
     public void systemReady() {
         synchronized (mStatsLock) {
             // read historical stats from disk
             readStatsLocked();
         }
 
-        // watch other system services that claim interfaces
+        // watch for network interfaces to be claimed
         final IntentFilter ifaceFilter = new IntentFilter();
         ifaceFilter.addAction(CONNECTIVITY_ACTION);
         mContext.registerReceiver(mIfaceReceiver, ifaceFilter, CONNECTIVITY_INTERNAL, mHandler);
 
         // listen for periodic polling events
+        // TODO: switch to stronger internal permission
         final IntentFilter pollFilter = new IntentFilter(ACTION_NETWORK_STATS_POLL);
-        mContext.registerReceiver(mPollReceiver, pollFilter, UPDATE_DEVICE_STATS, mHandler);
+        mContext.registerReceiver(mPollReceiver, pollFilter, READ_NETWORK_USAGE_HISTORY, mHandler);
 
         // persist stats during clean shutdown
         final IntentFilter shutdownFilter = new IntentFilter(Intent.ACTION_SHUTDOWN);
@@ -204,10 +209,6 @@
         }
     }
 
-    public void bindConnectivityManager(IConnectivityManager connManager) {
-        mConnManager = checkNotNull(connManager, "missing IConnectivityManager");
-    }
-
     private void shutdownLocked() {
         mContext.unregisterReceiver(mIfaceReceiver);
         mContext.unregisterReceiver(mPollReceiver);
@@ -237,8 +238,7 @@
 
     @Override
     public NetworkStatsHistory getHistoryForNetwork(int networkTemplate) {
-        // TODO: create relaxed permission for reading stats
-        mContext.enforceCallingOrSelfPermission(UPDATE_DEVICE_STATS, TAG);
+        mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG);
 
         synchronized (mStatsLock) {
             // combine all interfaces that match template
@@ -257,17 +257,41 @@
 
     @Override
     public NetworkStatsHistory getHistoryForUid(int uid, int networkTemplate) {
-        // TODO: create relaxed permission for reading stats
-        mContext.enforceCallingOrSelfPermission(UPDATE_DEVICE_STATS, TAG);
+        mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG);
 
         // TODO: return history for requested uid
         return null;
     }
 
     @Override
+    public NetworkStats getSummaryForNetwork(
+            long start, long end, int networkTemplate, String subscriberId) {
+        mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG);
+
+        synchronized (mStatsLock) {
+            long rx = 0;
+            long tx = 0;
+            long[] networkTotal = new long[2];
+
+            // combine total from all interfaces that match template
+            for (InterfaceIdentity ident : mSummaryStats.keySet()) {
+                final NetworkStatsHistory history = mSummaryStats.get(ident);
+                if (ident.matchesTemplate(networkTemplate, subscriberId)) {
+                    networkTotal = history.getTotalData(start, end, networkTotal);
+                    rx += networkTotal[0];
+                    tx += networkTotal[1];
+                }
+            }
+
+            final NetworkStats.Builder stats = new NetworkStats.Builder(end - start, 1);
+            stats.addEntry(IFACE_ALL, UID_ALL, tx, tx);
+            return stats.build();
+        }
+    }
+
+    @Override
     public NetworkStats getSummaryForAllUid(long start, long end, int networkTemplate) {
-        // TODO: create relaxed permission for reading stats
-        mContext.enforceCallingOrSelfPermission(UPDATE_DEVICE_STATS, TAG);
+        mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG);
 
         // TODO: apply networktemplate once granular uid stats are stored.
 
@@ -275,11 +299,11 @@
             final int size = mDetailStats.size();
             final NetworkStats.Builder stats = new NetworkStats.Builder(end - start, size);
 
-            final long[] total = new long[2];
+            long[] total = new long[2];
             for (int i = 0; i < size; i++) {
                 final int uid = mDetailStats.keyAt(i);
                 final NetworkStatsHistory history = mDetailStats.valueAt(i);
-                history.getTotalData(start, end, total);
+                total = history.getTotalData(start, end, total);
                 stats.addEntry(IFACE_ALL, uid, total[0], total[1]);
             }
             return stats.build();
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 295d569..151fde7 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -26,6 +26,8 @@
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
     <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" />
     <uses-permission android:name="android.permission.MANAGE_APP_TOKENS" />
+    <uses-permission android:name="android.permission.MANAGE_NETWORK_POLICY" />
+    <uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" />
 
     <application>
         <uses-library android:name="android.test.runner" />
diff --git a/services/tests/servicestests/src/com/android/server/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkPolicyManagerServiceTest.java
index 6552cdf..c4ddb00 100644
--- a/services/tests/servicestests/src/com/android/server/NetworkPolicyManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/NetworkPolicyManagerServiceTest.java
@@ -16,10 +16,15 @@
 
 package com.android.server;
 
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.NetworkPolicyManager.POLICY_NONE;
 import static android.net.NetworkPolicyManager.POLICY_REJECT_PAID_BACKGROUND;
 import static android.net.NetworkPolicyManager.RULE_ALLOW_ALL;
 import static android.net.NetworkPolicyManager.RULE_REJECT_PAID;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.TrafficStats.TEMPLATE_WIFI;
+import static com.android.server.net.NetworkPolicyManagerService.computeLastCycleBoundary;
 import static org.easymock.EasyMock.capture;
 import static org.easymock.EasyMock.createMock;
 import static org.easymock.EasyMock.eq;
@@ -31,20 +36,30 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
+import android.net.IConnectivityManager;
 import android.net.INetworkPolicyListener;
 import android.net.INetworkStatsService;
+import android.net.LinkProperties;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkPolicy;
+import android.net.NetworkState;
+import android.net.NetworkStats;
 import android.os.Binder;
 import android.os.IPowerManager;
 import android.test.AndroidTestCase;
 import android.test.mock.MockPackageManager;
 import android.test.suitebuilder.annotation.LargeTest;
 import android.test.suitebuilder.annotation.Suppress;
+import android.text.format.Time;
+import android.util.TrustedTime;
 
 import com.android.server.net.NetworkPolicyManagerService;
 
 import org.easymock.Capture;
 import org.easymock.EasyMock;
 
+import java.io.File;
 import java.util.concurrent.Future;
 
 /**
@@ -54,12 +69,18 @@
 public class NetworkPolicyManagerServiceTest extends AndroidTestCase {
     private static final String TAG = "NetworkPolicyManagerServiceTest";
 
+    private static final long TEST_START = 1194220800000L;
+    private static final String TEST_IFACE = "test0";
+
     private BroadcastInterceptingContext mServiceContext;
+    private File mPolicyDir;
 
     private IActivityManager mActivityManager;
     private IPowerManager mPowerManager;
     private INetworkStatsService mStatsService;
     private INetworkPolicyListener mPolicyListener;
+    private TrustedTime mTime;
+    private IConnectivityManager mConnManager;
 
     private NetworkPolicyManagerService mService;
     private IProcessObserver mProcessObserver;
@@ -90,13 +111,18 @@
             }
         };
 
+        mPolicyDir = getContext().getFilesDir();
+
         mActivityManager = createMock(IActivityManager.class);
         mPowerManager = createMock(IPowerManager.class);
         mStatsService = createMock(INetworkStatsService.class);
         mPolicyListener = createMock(INetworkPolicyListener.class);
+        mTime = createMock(TrustedTime.class);
+        mConnManager = createMock(IConnectivityManager.class);
 
         mService = new NetworkPolicyManagerService(
-                mServiceContext, mActivityManager, mPowerManager, mStatsService);
+                mServiceContext, mActivityManager, mPowerManager, mStatsService, mTime, mPolicyDir);
+        mService.bindConnectivityManager(mConnManager);
 
         // RemoteCallbackList needs a binder to use as key
         expect(mPolicyListener.asBinder()).andReturn(mStubBinder).atLeastOnce();
@@ -122,12 +148,18 @@
 
     @Override
     public void tearDown() throws Exception {
+        for (File file : mPolicyDir.listFiles()) {
+            file.delete();
+        }
+
         mServiceContext = null;
+        mPolicyDir = null;
 
         mActivityManager = null;
         mPowerManager = null;
         mStatsService = null;
         mPolicyListener = null;
+        mTime = null;
 
         mService = null;
         mProcessObserver = null;
@@ -260,17 +292,120 @@
         verifyAndReset();
     }
 
+    public void testLastCycleBoundaryThisMonth() throws Exception {
+        // assume cycle day of "5th", which should be in same month
+        final long currentTime = parseTime("2007-11-14T00:00:00.000Z");
+        final long expectedCycle = parseTime("2007-11-05T00:00:00.000Z");
+
+        final NetworkPolicy policy = new NetworkPolicy(5, 1024L, 1024L);
+        final long actualCycle = computeLastCycleBoundary(currentTime, policy);
+        assertEquals(expectedCycle, actualCycle);
+    }
+
+    public void testLastCycleBoundaryLastMonth() throws Exception {
+        // assume cycle day of "20th", which should be in last month
+        final long currentTime = parseTime("2007-11-14T00:00:00.000Z");
+        final long expectedCycle = parseTime("2007-10-20T00:00:00.000Z");
+
+        final NetworkPolicy policy = new NetworkPolicy(20, 1024L, 1024L);
+        final long actualCycle = computeLastCycleBoundary(currentTime, policy);
+        assertEquals(expectedCycle, actualCycle);
+    }
+
+    public void testLastCycleBoundaryThisMonthFebruary() throws Exception {
+        // assume cycle day of "30th" in february; should go to january
+        final long currentTime = parseTime("2007-02-14T00:00:00.000Z");
+        final long expectedCycle = parseTime("2007-01-30T00:00:00.000Z");
+
+        final NetworkPolicy policy = new NetworkPolicy(30, 1024L, 1024L);
+        final long actualCycle = computeLastCycleBoundary(currentTime, policy);
+        assertEquals(expectedCycle, actualCycle);
+    }
+
+    public void testLastCycleBoundaryLastMonthFebruary() throws Exception {
+        // assume cycle day of "30th" in february, which should clamp
+        final long currentTime = parseTime("2007-03-14T00:00:00.000Z");
+        final long expectedCycle = parseTime("2007-03-01T00:00:00.000Z");
+
+        final NetworkPolicy policy = new NetworkPolicy(30, 1024L, 1024L);
+        final long actualCycle = computeLastCycleBoundary(currentTime, policy);
+        assertEquals(expectedCycle, actualCycle);
+    }
+
+    public void testNetworkPolicyAppliedCycleLastMonth() throws Exception {
+        long elapsedRealtime = 0;
+        NetworkState[] state = null;
+        NetworkStats stats = null;
+
+        final long TIME_FEB_15 = 1171497600000L;
+        final long TIME_MAR_10 = 1173484800000L;
+        final int CYCLE_DAY = 15;
+
+        // first, pretend that wifi network comes online. no policy active,
+        // which means we shouldn't push limit to interface.
+        state = new NetworkState[] { buildWifi() };
+        expect(mConnManager.getAllNetworkState()).andReturn(state).atLeastOnce();
+        expectTime(TIME_MAR_10 + elapsedRealtime);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION));
+        verifyAndReset();
+
+        // now change cycle to be on 15th, and test in early march, to verify we
+        // pick cycle day in previous month.
+        expect(mConnManager.getAllNetworkState()).andReturn(state).atLeastOnce();
+        expectTime(TIME_MAR_10 + elapsedRealtime);
+
+        // pretend that 512 bytes total have happened
+        stats = new NetworkStats.Builder(elapsedRealtime, 1).addEntry(
+                TEST_IFACE, UID_ALL, 256L, 256L).build();
+        expect(mStatsService.getSummaryForNetwork(TIME_FEB_15, TIME_MAR_10, TEMPLATE_WIFI, null))
+                .andReturn(stats).atLeastOnce();
+
+        // expect that quota remaining should be 1536 bytes
+        // TODO: write up NetworkManagementService mock
+
+        replay();
+        mService.setNetworkPolicy(TEMPLATE_WIFI, null, new NetworkPolicy(CYCLE_DAY, 1024L, 2048L));
+        verifyAndReset();
+    }
+
+    private static long parseTime(String time) {
+        final Time result = new Time();
+        result.parse3339(time);
+        return result.toMillis(true);
+    }
+
+    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 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 expectRulesChanged(int uid, int policy) throws Exception {
         mPolicyListener.onRulesChanged(eq(uid), eq(policy));
         expectLastCall().atLeastOnce();
     }
 
     private void replay() {
-        EasyMock.replay(mActivityManager, mPowerManager, mStatsService, mPolicyListener);
+        EasyMock.replay(mActivityManager, mPowerManager, mStatsService, mPolicyListener, mTime,
+                mConnManager);
     }
 
     private void verifyAndReset() {
-        EasyMock.verify(mActivityManager, mPowerManager, mStatsService, mPolicyListener);
-        EasyMock.reset(mActivityManager, mPowerManager, mStatsService, mPolicyListener);
+        EasyMock.verify(mActivityManager, mPowerManager, mStatsService, mPolicyListener, mTime,
+                mConnManager);
+        EasyMock.reset(mActivityManager, mPowerManager, mStatsService, mPolicyListener, mTime,
+                mConnManager);
     }
 }