Merge "Disable always-on VPN in factoryReset"
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index a45e6f5..042481f 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -223,6 +223,13 @@
     public static final String EXTRA_CAPTIVE_PORTAL_URL = "android.net.extra.CAPTIVE_PORTAL_URL";
 
     /**
+     * Key for passing a user agent string to the captive portal login activity.
+     * {@hide}
+     */
+    public static final String EXTRA_CAPTIVE_PORTAL_USER_AGENT =
+            "android.net.extra.CAPTIVE_PORTAL_USER_AGENT";
+
+    /**
      * Broadcast action to indicate the change of data activity status
      * (idle or active) on a network in a recent period.
      * The network becomes active when data transmission is started, or
@@ -343,6 +350,15 @@
     public static final String ACTION_PROMPT_UNVALIDATED = "android.net.conn.PROMPT_UNVALIDATED";
 
     /**
+     * Action used to display a dialog that asks the user whether to avoid a network that is no
+     * longer validated. This intent is used to start the dialog in settings via startActivity.
+     *
+     * @hide
+     */
+    public static final String ACTION_PROMPT_LOST_VALIDATION =
+            "android.net.conn.PROMPT_LOST_VALIDATION";
+
+    /**
      * Invalid tethering type.
      * @see #startTethering(int, OnStartTetheringCallback, boolean)
      * @hide
@@ -1035,6 +1051,26 @@
     }
 
     /**
+     * Request that this callback be invoked at ConnectivityService's earliest
+     * convenience with the current satisfying network's LinkProperties.
+     * If no such network exists no callback invocation is performed.
+     *
+     * The callback must have been registered with #requestNetwork() or
+     * #registerDefaultNetworkCallback(); callbacks registered with
+     * registerNetworkCallback() are not specific to any particular Network so
+     * do not cause any updates.
+     *
+     * @hide
+     */
+    public void requestLinkProperties(NetworkCallback networkCallback) {
+        try {
+            mService.requestLinkProperties(networkCallback.networkRequest);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Get the {@link android.net.NetworkCapabilities} for the given {@link Network}.  This
      * will return {@code null} if the network is unknown.
      * <p>This method requires the caller to hold the permission
@@ -1052,6 +1088,26 @@
     }
 
     /**
+     * Request that this callback be invoked at ConnectivityService's earliest
+     * convenience with the current satisfying network's NetworkCapabilities.
+     * If no such network exists no callback invocation is performed.
+     *
+     * The callback must have been registered with #requestNetwork() or
+     * #registerDefaultNetworkCallback(); callbacks registered with
+     * registerNetworkCallback() are not specific to any particular Network so
+     * do not cause any updates.
+     *
+     * @hide
+     */
+    public void requestNetworkCapabilities(NetworkCallback networkCallback) {
+        try {
+            mService.requestNetworkCapabilities(networkCallback.networkRequest);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Gets the URL that should be used for resolving whether a captive portal is present.
      * 1. This URL should respond with a 204 response to a GET request to indicate no captive
      *    portal is present.
@@ -1776,6 +1832,16 @@
         return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
     }
 
+    /* TODO: These permissions checks don't belong in client-side code. Move them to
+     * services.jar, possibly in com.android.server.net. */
+
+    /** {@hide} */
+    public static final boolean checkChangePermission(Context context) {
+        int uid = Binder.getCallingUid();
+        return Settings.checkAndNoteChangeNetworkStateOperation(context, uid, Settings
+                .getPackageNameForUid(context, uid), false /* throwException */);
+    }
+
     /** {@hide} */
     public static final void enforceChangePermission(Context context) {
         int uid = Binder.getCallingUid();
@@ -2020,6 +2086,8 @@
     @SystemApi
     public void startTethering(int type, boolean showProvisioningUi,
             final OnStartTetheringCallback callback, Handler handler) {
+        checkNotNull(callback, "OnStartTetheringCallback cannot be null.");
+
         ResultReceiver wrappedCallback = new ResultReceiver(handler) {
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
@@ -2030,6 +2098,7 @@
                 }
             }
         };
+
         try {
             mService.startTethering(type, wrappedCallback, showProvisioningUi);
         } catch (RemoteException e) {
@@ -2533,7 +2602,8 @@
 
         /**
          * Called if no network is found in the given timeout time.  If no timeout is given,
-         * this will not be called.
+         * this will not be called. The associated {@link NetworkRequest} will have already
+         * been removed and released, as if {@link #unregisterNetworkCallback} had been called.
          * @hide
          */
         public void onUnavailable() {}
@@ -2597,6 +2667,7 @@
     public static final int CALLBACK_IP_CHANGED          = BASE + 7;
     /** @hide */
     public static final int CALLBACK_RELEASED            = BASE + 8;
+    // TODO: consider deleting CALLBACK_EXIT and shifting following enum codes down by 1.
     /** @hide */
     public static final int CALLBACK_EXIT                = BASE + 9;
     /** @hide obj = NetworkCapabilities, arg1 = seq number */
@@ -2606,25 +2677,38 @@
     /** @hide */
     public static final int CALLBACK_RESUMED             = BASE + 12;
 
+    /** @hide */
+    public static String getCallbackName(int whichCallback) {
+        switch (whichCallback) {
+            case CALLBACK_PRECHECK:     return "CALLBACK_PRECHECK";
+            case CALLBACK_AVAILABLE:    return "CALLBACK_AVAILABLE";
+            case CALLBACK_LOSING:       return "CALLBACK_LOSING";
+            case CALLBACK_LOST:         return "CALLBACK_LOST";
+            case CALLBACK_UNAVAIL:      return "CALLBACK_UNAVAIL";
+            case CALLBACK_CAP_CHANGED:  return "CALLBACK_CAP_CHANGED";
+            case CALLBACK_IP_CHANGED:   return "CALLBACK_IP_CHANGED";
+            case CALLBACK_RELEASED:     return "CALLBACK_RELEASED";
+            case CALLBACK_EXIT:         return "CALLBACK_EXIT";
+            case EXPIRE_LEGACY_REQUEST: return "EXPIRE_LEGACY_REQUEST";
+            case CALLBACK_SUSPENDED:    return "CALLBACK_SUSPENDED";
+            case CALLBACK_RESUMED:      return "CALLBACK_RESUMED";
+            default:
+                return Integer.toString(whichCallback);
+        }
+    }
+
     private class CallbackHandler extends Handler {
-        private final HashMap<NetworkRequest, NetworkCallback>mCallbackMap;
-        private final AtomicInteger mRefCount;
         private static final String TAG = "ConnectivityManager.CallbackHandler";
-        private final ConnectivityManager mCm;
         private static final boolean DBG = false;
 
-        CallbackHandler(Looper looper, HashMap<NetworkRequest, NetworkCallback>callbackMap,
-                AtomicInteger refCount, ConnectivityManager cm) {
+        CallbackHandler(Looper looper) {
             super(looper);
-            mCallbackMap = callbackMap;
-            mRefCount = refCount;
-            mCm = cm;
         }
 
         @Override
         public void handleMessage(Message message) {
-            NetworkRequest request = (NetworkRequest) getObject(message, NetworkRequest.class);
-            Network network = (Network) getObject(message, Network.class);
+            NetworkRequest request = getObject(message, NetworkRequest.class);
+            Network network = getObject(message, Network.class);
             if (DBG) {
                 Log.d(TAG, whatToString(message.what) + " for network " + network);
             }
@@ -2667,9 +2751,7 @@
                 case CALLBACK_CAP_CHANGED: {
                     NetworkCallback callback = getCallback(request, "CAP_CHANGED");
                     if (callback != null) {
-                        NetworkCapabilities cap = (NetworkCapabilities)getObject(message,
-                                NetworkCapabilities.class);
-
+                        NetworkCapabilities cap = getObject(message, NetworkCapabilities.class);
                         callback.onCapabilitiesChanged(network, cap);
                     }
                     break;
@@ -2677,9 +2759,7 @@
                 case CALLBACK_IP_CHANGED: {
                     NetworkCallback callback = getCallback(request, "IP_CHANGED");
                     if (callback != null) {
-                        LinkProperties lp = (LinkProperties)getObject(message,
-                                LinkProperties.class);
-
+                        LinkProperties lp = getObject(message, LinkProperties.class);
                         callback.onLinkPropertiesChanged(network, lp);
                     }
                     break;
@@ -2699,24 +2779,16 @@
                     break;
                 }
                 case CALLBACK_RELEASED: {
-                    NetworkCallback callback = null;
-                    synchronized(mCallbackMap) {
-                        callback = mCallbackMap.remove(request);
+                    final NetworkCallback callback;
+                    synchronized(sCallbacks) {
+                        callback = sCallbacks.remove(request);
                     }
-                    if (callback != null) {
-                        synchronized(mRefCount) {
-                            if (mRefCount.decrementAndGet() == 0) {
-                                getLooper().quit();
-                            }
-                        }
-                    } else {
+                    if (callback == null) {
                         Log.e(TAG, "callback not found for RELEASED message");
                     }
                     break;
                 }
                 case CALLBACK_EXIT: {
-                    Log.d(TAG, "Listener quitting");
-                    getLooper().quit();
                     break;
                 }
                 case EXPIRE_LEGACY_REQUEST: {
@@ -2726,14 +2798,14 @@
             }
         }
 
-        private Object getObject(Message msg, Class c) {
-            return msg.getData().getParcelable(c.getSimpleName());
+        private <T> T getObject(Message msg, Class<T> c) {
+            return (T) msg.getData().getParcelable(c.getSimpleName());
         }
 
         private NetworkCallback getCallback(NetworkRequest req, String name) {
             NetworkCallback callback;
-            synchronized(mCallbackMap) {
-                callback = mCallbackMap.get(req);
+            synchronized(sCallbacks) {
+                callback = sCallbacks.get(req);
             }
             if (callback == null) {
                 Log.e(TAG, "callback not found for " + name + " message");
@@ -2742,63 +2814,56 @@
         }
     }
 
-    private void incCallbackHandlerRefCount() {
-        synchronized(sCallbackRefCount) {
-            if (sCallbackRefCount.incrementAndGet() == 1) {
-                // TODO: switch this to ConnectivityThread
-                HandlerThread callbackThread = new HandlerThread("ConnectivityManager");
-                callbackThread.start();
-                sCallbackHandler = new CallbackHandler(callbackThread.getLooper(),
-                        sNetworkCallback, sCallbackRefCount, this);
+    private CallbackHandler getHandler() {
+        synchronized (sCallbacks) {
+            if (sCallbackHandler == null) {
+                sCallbackHandler = new CallbackHandler(ConnectivityThread.getInstanceLooper());
             }
+            return sCallbackHandler;
         }
     }
 
-    private void decCallbackHandlerRefCount() {
-        synchronized(sCallbackRefCount) {
-            if (sCallbackRefCount.decrementAndGet() == 0) {
-                sCallbackHandler.obtainMessage(CALLBACK_EXIT).sendToTarget();
-                sCallbackHandler = null;
-            }
-        }
-    }
-
-    static final HashMap<NetworkRequest, NetworkCallback> sNetworkCallback =
-            new HashMap<NetworkRequest, NetworkCallback>();
-    static final AtomicInteger sCallbackRefCount = new AtomicInteger(0);
-    static CallbackHandler sCallbackHandler = null;
+    static final HashMap<NetworkRequest, NetworkCallback> sCallbacks = new HashMap<>();
+    static CallbackHandler sCallbackHandler;
 
     private final static int LISTEN  = 1;
     private final static int REQUEST = 2;
 
     private NetworkRequest sendRequestForNetwork(NetworkCapabilities need,
-            NetworkCallback networkCallback, int timeoutSec, int action,
-            int legacyType) {
-        if (networkCallback == null) {
+            NetworkCallback callback, int timeoutMs, int action, int legacyType) {
+        return sendRequestForNetwork(need, callback, getHandler(), timeoutMs, action, legacyType);
+    }
+
+    private NetworkRequest sendRequestForNetwork(NetworkCapabilities need,
+            NetworkCallback callback, Handler handler, int timeoutMs, int action, int legacyType) {
+        if (callback == null) {
             throw new IllegalArgumentException("null NetworkCallback");
         }
         if (need == null && action != REQUEST) {
             throw new IllegalArgumentException("null NetworkCapabilities");
         }
+        // TODO: throw an exception if callback.networkRequest is not null.
+        // http://b/20701525
+        final NetworkRequest request;
         try {
-            incCallbackHandlerRefCount();
-            synchronized(sNetworkCallback) {
+            synchronized(sCallbacks) {
+                Messenger messenger = new Messenger(handler);
+                Binder binder = new Binder();
                 if (action == LISTEN) {
-                    networkCallback.networkRequest = mService.listenForNetwork(need,
-                            new Messenger(sCallbackHandler), new Binder());
+                    request = mService.listenForNetwork(need, messenger, binder);
                 } else {
-                    networkCallback.networkRequest = mService.requestNetwork(need,
-                            new Messenger(sCallbackHandler), timeoutSec, new Binder(), legacyType);
+                    request = mService.requestNetwork(
+                            need, messenger, timeoutMs, binder, legacyType);
                 }
-                if (networkCallback.networkRequest != null) {
-                    sNetworkCallback.put(networkCallback.networkRequest, networkCallback);
+                if (request != null) {
+                    sCallbacks.put(request, callback);
                 }
+                callback.networkRequest = request;
             }
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
-        if (networkCallback.networkRequest == null) decCallbackHandlerRefCount();
-        return networkCallback.networkRequest;
+        return request;
     }
 
     /**
@@ -3103,14 +3168,11 @@
             throw new IllegalArgumentException("Invalid NetworkCallback");
         }
         try {
+            // CallbackHandler will release callback when receiving CALLBACK_RELEASED.
             mService.releaseNetworkRequest(networkCallback.networkRequest);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
-
-        synchronized (sNetworkCallback) {
-            sNetworkCallback.remove(networkCallback.networkRequest);
-        }
     }
 
     /**
@@ -3152,6 +3214,27 @@
     }
 
     /**
+     * Informs the system to penalize {@code network}'s score when it becomes unvalidated. This is
+     * only meaningful if the system is configured not to penalize such networks, e.g., if the
+     * {@code config_networkAvoidBadWifi} configuration variable is set to 0 and the {@code
+     * NETWORK_AVOID_BAD_WIFI setting is unset}.
+     *
+     * <p>This method requires the caller to hold the permission
+     * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL}
+     *
+     * @param network The network to accept.
+     *
+     * @hide
+     */
+    public void setAvoidUnvalidated(Network network) {
+        try {
+            mService.setAvoidUnvalidated(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Resets all connectivity manager settings back to factory defaults.
      * @hide
      */
diff --git a/core/java/android/net/ConnectivityThread.java b/core/java/android/net/ConnectivityThread.java
index 55c3402..0b218e7 100644
--- a/core/java/android/net/ConnectivityThread.java
+++ b/core/java/android/net/ConnectivityThread.java
@@ -27,25 +27,30 @@
  * @hide
  */
 public final class ConnectivityThread extends HandlerThread {
-    private static ConnectivityThread sInstance;
+
+    // A class implementing the lazy holder idiom: the unique static instance
+    // of ConnectivityThread is instantiated in a thread-safe way (guaranteed by
+    // the language specs) the first time that Singleton is referenced in get()
+    // or getInstanceLooper().
+    private static class Singleton {
+        private static final ConnectivityThread INSTANCE = createInstance();
+    }
 
     private ConnectivityThread() {
         super("ConnectivityThread");
     }
 
-    private static synchronized ConnectivityThread getInstance() {
-        if (sInstance == null) {
-            sInstance = new ConnectivityThread();
-            sInstance.start();
-        }
-        return sInstance;
+    private static ConnectivityThread createInstance() {
+        ConnectivityThread t = new ConnectivityThread();
+        t.start();
+        return t;
     }
 
     public static ConnectivityThread get() {
-        return getInstance();
+        return Singleton.INSTANCE;
     }
 
     public static Looper getInstanceLooper() {
-        return getInstance().getLooper();
+        return Singleton.INSTANCE.getLooper();
     }
 }
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index 0d518cc..4aabda9 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -156,9 +156,12 @@
     void pendingListenForNetwork(in NetworkCapabilities networkCapabilities,
             in PendingIntent operation);
 
+    void requestLinkProperties(in NetworkRequest networkRequest);
+    void requestNetworkCapabilities(in NetworkRequest networkRequest);
     void releaseNetworkRequest(in NetworkRequest networkRequest);
 
     void setAcceptUnvalidated(in Network network, boolean accept, boolean always);
+    void setAvoidUnvalidated(in Network network);
 
     int getRestoreDefaultNetworkDelay(int networkType);
 
diff --git a/core/java/android/net/LinkAddress.java b/core/java/android/net/LinkAddress.java
index 384ab1c..6e74f14 100644
--- a/core/java/android/net/LinkAddress.java
+++ b/core/java/android/net/LinkAddress.java
@@ -103,7 +103,7 @@
     private boolean isIPv6ULA() {
         if (address != null && address instanceof Inet6Address) {
             byte[] bytes = address.getAddress();
-            return ((bytes[0] & (byte)0xfc) == (byte)0xfc);
+            return ((bytes[0] & (byte)0xfe) == (byte)0xfc);
         }
         return false;
     }
diff --git a/core/java/android/net/NetworkCapabilities.java b/core/java/android/net/NetworkCapabilities.java
index 6243f46..4dd8ce9 100644
--- a/core/java/android/net/NetworkCapabilities.java
+++ b/core/java/android/net/NetworkCapabilities.java
@@ -182,8 +182,15 @@
      */
     public static final int NET_CAPABILITY_CAPTIVE_PORTAL = 17;
 
+    /**
+     * Indicates that this network is available for use by apps, and not a network that is being
+     * kept up in the background to facilitate fast network switching.
+     * @hide
+     */
+    public static final int NET_CAPABILITY_FOREGROUND = 18;
+
     private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS;
-    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_CAPTIVE_PORTAL;
+    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_FOREGROUND;
 
     /**
      * Network capabilities that are expected to be mutable, i.e., can change while a particular
@@ -194,7 +201,8 @@
             // http://b/18206275
             (1 << NET_CAPABILITY_TRUSTED) |
             (1 << NET_CAPABILITY_VALIDATED) |
-            (1 << NET_CAPABILITY_CAPTIVE_PORTAL);
+            (1 << NET_CAPABILITY_CAPTIVE_PORTAL) |
+            (1 << NET_CAPABILITY_FOREGROUND);
 
     /**
      * Network specifier for factories which want to match any network specifier
@@ -217,8 +225,7 @@
      * get immediately torn down because they do not have the requested capability.
      */
     private static final long NON_REQUESTABLE_CAPABILITIES =
-            (1 << NET_CAPABILITY_VALIDATED) |
-            (1 << NET_CAPABILITY_CAPTIVE_PORTAL);
+            MUTABLE_CAPABILITIES & ~(1 << NET_CAPABILITY_TRUSTED);
 
     /**
      * Capabilities that are set by default when the object is constructed.
@@ -325,6 +332,7 @@
     public String describeFirstNonRequestableCapability() {
         if (hasCapability(NET_CAPABILITY_VALIDATED)) return "NET_CAPABILITY_VALIDATED";
         if (hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) return "NET_CAPABILITY_CAPTIVE_PORTAL";
+        if (hasCapability(NET_CAPABILITY_FOREGROUND)) return "NET_CAPABILITY_FOREGROUND";
         // This cannot happen unless the preceding checks are incomplete.
         if ((mNetworkCapabilities & NON_REQUESTABLE_CAPABILITIES) != 0) {
             return "unknown non-requestable capabilities " + Long.toHexString(mNetworkCapabilities);
@@ -352,6 +360,11 @@
                 (that.mNetworkCapabilities & ~MUTABLE_CAPABILITIES));
     }
 
+    private boolean equalsNetCapabilitiesRequestable(NetworkCapabilities that) {
+        return ((this.mNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES) ==
+                (that.mNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES));
+    }
+
     /**
      * Removes the NET_CAPABILITY_NOT_RESTRICTED capability if all the capabilities it provides are
      * typically provided by restricted networks.
@@ -405,8 +418,13 @@
      */
     public static final int TRANSPORT_VPN = 4;
 
+    /**
+     * Indicates this network uses a Wi-Fi Aware transport.
+     */
+    public static final int TRANSPORT_WIFI_AWARE = 5;
+
     private static final int MIN_TRANSPORT = TRANSPORT_CELLULAR;
-    private static final int MAX_TRANSPORT = TRANSPORT_VPN;
+    private static final int MAX_TRANSPORT = TRANSPORT_WIFI_AWARE;
 
     /**
      * Adds the given transport type to this {@code NetworkCapability} instance.
@@ -749,6 +767,19 @@
                 equalsSpecifier(nc));
     }
 
+    /**
+     * Checks that our requestable capabilities are the same as those of the given
+     * {@code NetworkCapabilities}.
+     *
+     * @hide
+     */
+    public boolean equalRequestableCapabilities(NetworkCapabilities nc) {
+        if (nc == null) return false;
+        return (equalsNetCapabilitiesRequestable(nc) &&
+                equalsTransportTypes(nc) &&
+                equalsSpecifier(nc));
+    }
+
     @Override
     public boolean equals(Object obj) {
         if (obj == null || (obj instanceof NetworkCapabilities == false)) return false;
@@ -833,6 +864,7 @@
                 case NET_CAPABILITY_NOT_VPN:        capabilities += "NOT_VPN"; break;
                 case NET_CAPABILITY_VALIDATED:      capabilities += "VALIDATED"; break;
                 case NET_CAPABILITY_CAPTIVE_PORTAL: capabilities += "CAPTIVE_PORTAL"; break;
+                case NET_CAPABILITY_FOREGROUND:     capabilities += "FOREGROUND"; break;
             }
             if (++i < types.length) capabilities += "&";
         }
@@ -862,6 +894,7 @@
                 case TRANSPORT_BLUETOOTH:   transports += "BLUETOOTH"; break;
                 case TRANSPORT_ETHERNET:    transports += "ETHERNET"; break;
                 case TRANSPORT_VPN:         transports += "VPN"; break;
+                case TRANSPORT_WIFI_AWARE:  transports += "WIFI_AWARE"; break;
             }
             if (++i < types.length) transports += "|";
         }
diff --git a/core/java/android/net/NetworkMisc.java b/core/java/android/net/NetworkMisc.java
index 5511a24..69f50a2 100644
--- a/core/java/android/net/NetworkMisc.java
+++ b/core/java/android/net/NetworkMisc.java
@@ -52,6 +52,15 @@
     public boolean acceptUnvalidated;
 
     /**
+     * Set to avoid surfacing the "Sign in to network" notification.
+     * if carrier receivers/apps are registered to handle the carrier-specific provisioning
+     * procedure, a carrier specific provisioning notification will be placed.
+     * only one notification should be displayed. This field is set based on
+     * which notification should be used for provisioning.
+     */
+    public boolean provisioningNotificationDisabled;
+
+    /**
      * For mobile networks, this is the subscriber ID (such as IMSI).
      */
     public String subscriberId;
@@ -65,6 +74,7 @@
             explicitlySelected = nm.explicitlySelected;
             acceptUnvalidated = nm.acceptUnvalidated;
             subscriberId = nm.subscriberId;
+            provisioningNotificationDisabled = nm.provisioningNotificationDisabled;
         }
     }
 
@@ -79,6 +89,7 @@
         out.writeInt(explicitlySelected ? 1 : 0);
         out.writeInt(acceptUnvalidated ? 1 : 0);
         out.writeString(subscriberId);
+        out.writeInt(provisioningNotificationDisabled ? 1 : 0);
     }
 
     public static final Creator<NetworkMisc> CREATOR = new Creator<NetworkMisc>() {
@@ -89,6 +100,7 @@
             networkMisc.explicitlySelected = in.readInt() != 0;
             networkMisc.acceptUnvalidated = in.readInt() != 0;
             networkMisc.subscriberId = in.readString();
+            networkMisc.provisioningNotificationDisabled = in.readInt() != 0;
             return networkMisc;
         }
 
diff --git a/core/java/android/net/NetworkRequest.java b/core/java/android/net/NetworkRequest.java
index f1edcbe..ae72470 100644
--- a/core/java/android/net/NetworkRequest.java
+++ b/core/java/android/net/NetworkRequest.java
@@ -19,6 +19,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.util.Objects;
+
 /**
  * Defines a request for a network, made through {@link NetworkRequest.Builder} and used
  * to request a network via {@link ConnectivityManager#requestNetwork} or listen for changes
@@ -47,15 +49,69 @@
     public final int legacyType;
 
     /**
+     * A NetworkRequest as used by the system can be one of the following types:
+     *
+     *     - LISTEN, for which the framework will issue callbacks about any
+     *       and all networks that match the specified NetworkCapabilities,
+     *
+     *     - REQUEST, capable of causing a specific network to be created
+     *       first (e.g. a telephony DUN request), the framework will issue
+     *       callbacks about the single, highest scoring current network
+     *       (if any) that matches the specified NetworkCapabilities, or
+     *
+     *     - TRACK_DEFAULT, a hybrid of the two designed such that the
+     *       framework will issue callbacks for the single, highest scoring
+     *       current network (if any) that matches the capabilities of the
+     *       default Internet request (mDefaultRequest), but which cannot cause
+     *       the framework to either create or retain the existence of any
+     *       specific network. Note that from the point of view of the request
+     *       matching code, TRACK_DEFAULT is identical to REQUEST: its special
+     *       behaviour is not due to different semantics, but to the fact that
+     *       the system will only ever create a TRACK_DEFAULT with capabilities
+     *       that are identical to the default request's capabilities, thus
+     *       causing it to share fate in every way with the default request.
+     *
+     *     - BACKGROUND_REQUEST, like REQUEST but does not cause any networks
+     *       to retain the NET_CAPABILITY_FOREGROUND capability. A network with
+     *       no foreground requests is in the background. A network that has
+     *       one or more background requests and loses its last foreground
+     *       request to a higher-scoring network will not go into the
+     *       background immediately, but will linger and go into the background
+     *       after the linger timeout.
+     *
+     *     - The value NONE is used only by applications. When an application
+     *       creates a NetworkRequest, it does not have a type; the type is set
+     *       by the system depending on the method used to file the request
+     *       (requestNetwork, registerNetworkCallback, etc.).
+     *
      * @hide
      */
-    public NetworkRequest(NetworkCapabilities nc, int legacyType, int rId) {
+    public static enum Type {
+        NONE,
+        LISTEN,
+        TRACK_DEFAULT,
+        REQUEST,
+        BACKGROUND_REQUEST,
+    };
+
+    /**
+     * The type of the request. This is only used by the system and is always NONE elsewhere.
+     *
+     * @hide
+     */
+    public final Type type;
+
+    /**
+     * @hide
+     */
+    public NetworkRequest(NetworkCapabilities nc, int legacyType, int rId, Type type) {
         if (nc == null) {
             throw new NullPointerException();
         }
         requestId = rId;
         networkCapabilities = nc;
         this.legacyType = legacyType;
+        this.type = type;
     }
 
     /**
@@ -65,6 +121,7 @@
         networkCapabilities = new NetworkCapabilities(that.networkCapabilities);
         requestId = that.requestId;
         this.legacyType = that.legacyType;
+        this.type = that.type;
     }
 
     /**
@@ -90,14 +147,14 @@
             final NetworkCapabilities nc = new NetworkCapabilities(mNetworkCapabilities);
             nc.maybeMarkCapabilitiesRestricted();
             return new NetworkRequest(nc, ConnectivityManager.TYPE_NONE,
-                    ConnectivityManager.REQUEST_ID_UNSET);
+                    ConnectivityManager.REQUEST_ID_UNSET, Type.NONE);
         }
 
         /**
          * Add the given capability requirement to this builder.  These represent
          * the requested network's required capabilities.  Note that when searching
          * for a network to satisfy a request, all capabilities requested must be
-         * satisfied.  See {@link NetworkCapabilities} for {@code NET_CAPABILITIY_*}
+         * satisfied.  See {@link NetworkCapabilities} for {@code NET_CAPABILITY_*}
          * definitions.
          *
          * @param capability The {@code NetworkCapabilities.NET_CAPABILITY_*} to add.
@@ -223,6 +280,7 @@
         dest.writeParcelable(networkCapabilities, flags);
         dest.writeInt(legacyType);
         dest.writeInt(requestId);
+        dest.writeString(type.name());
     }
     public static final Creator<NetworkRequest> CREATOR =
         new Creator<NetworkRequest>() {
@@ -230,7 +288,8 @@
                 NetworkCapabilities nc = (NetworkCapabilities)in.readParcelable(null);
                 int legacyType = in.readInt();
                 int requestId = in.readInt();
-                NetworkRequest result = new NetworkRequest(nc, legacyType, requestId);
+                Type type = Type.valueOf(in.readString());  // IllegalArgumentException if invalid.
+                NetworkRequest result = new NetworkRequest(nc, legacyType, requestId, type);
                 return result;
             }
             public NetworkRequest[] newArray(int size) {
@@ -238,8 +297,64 @@
             }
         };
 
+    /**
+     * Returns true iff. this NetworkRequest is of type LISTEN.
+     *
+     * @hide
+     */
+    public boolean isListen() {
+        return type == Type.LISTEN;
+    }
+
+    /**
+     * Returns true iff. the contained NetworkRequest is one that:
+     *
+     *     - should be associated with at most one satisfying network
+     *       at a time;
+     *
+     *     - should cause a network to be kept up, but not necessarily in
+     *       the foreground, if it is the best network which can satisfy the
+     *       NetworkRequest.
+     *
+     * For full detail of how isRequest() is used for pairing Networks with
+     * NetworkRequests read rematchNetworkAndRequests().
+     *
+     * @hide
+     */
+    public boolean isRequest() {
+        return isForegroundRequest() || isBackgroundRequest();
+    }
+
+    /**
+     * Returns true iff. the contained NetworkRequest is one that:
+     *
+     *     - should be associated with at most one satisfying network
+     *       at a time;
+     *
+     *     - should cause a network to be kept up and in the foreground if
+     *       it is the best network which can satisfy the NetworkRequest.
+     *
+     * For full detail of how isRequest() is used for pairing Networks with
+     * NetworkRequests read rematchNetworkAndRequests().
+     *
+     * @hide
+     */
+    public boolean isForegroundRequest() {
+        return type == Type.TRACK_DEFAULT || type == Type.REQUEST;
+    }
+
+    /**
+     * Returns true iff. this NetworkRequest is of type BACKGROUND_REQUEST.
+     *
+     * @hide
+     */
+    public boolean isBackgroundRequest() {
+        return type == Type.BACKGROUND_REQUEST;
+    }
+
     public String toString() {
-        return "NetworkRequest [ id=" + requestId + ", legacyType=" + legacyType +
+        return "NetworkRequest [ " + type + " id=" + requestId +
+                (legacyType != ConnectivityManager.TYPE_NONE ? ", legacyType=" + legacyType : "") +
                 ", " + networkCapabilities.toString() + " ]";
     }
 
@@ -248,13 +363,11 @@
         NetworkRequest that = (NetworkRequest)obj;
         return (that.legacyType == this.legacyType &&
                 that.requestId == this.requestId &&
-                ((that.networkCapabilities == null && this.networkCapabilities == null) ||
-                 (that.networkCapabilities != null &&
-                  that.networkCapabilities.equals(this.networkCapabilities))));
+                that.type == this.type &&
+                Objects.equals(that.networkCapabilities, this.networkCapabilities));
     }
 
     public int hashCode() {
-        return requestId + (legacyType * 1013) +
-                (networkCapabilities.hashCode() * 1051);
+        return Objects.hash(requestId, legacyType, networkCapabilities, type);
     }
 }
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
index 141af3d..a677d73 100644
--- a/core/java/android/net/NetworkUtils.java
+++ b/core/java/android/net/NetworkUtils.java
@@ -45,13 +45,31 @@
     public native static void attachDhcpFilter(FileDescriptor fd) throws SocketException;
 
     /**
-     * Attaches a socket filter that accepts ICMP6 router advertisement packets to the given socket.
+     * Attaches a socket filter that accepts ICMPv6 router advertisements to the given socket.
      * @param fd the socket's {@link FileDescriptor}.
      * @param packetType the hardware address type, one of ARPHRD_*.
      */
     public native static void attachRaFilter(FileDescriptor fd, int packetType) throws SocketException;
 
     /**
+     * Attaches a socket filter that accepts L2-L4 signaling traffic required for IP connectivity.
+     *
+     * This includes: all ARP, ICMPv6 RS/RA/NS/NA messages, and DHCPv4 exchanges.
+     *
+     * @param fd the socket's {@link FileDescriptor}.
+     * @param packetType the hardware address type, one of ARPHRD_*.
+     */
+    public native static void attachControlPacketFilter(FileDescriptor fd, int packetType)
+            throws SocketException;
+
+    /**
+     * Configures a socket for receiving ICMPv6 router solicitations and sending advertisements.
+     * @param fd the socket's {@link FileDescriptor}.
+     * @param ifIndex the interface index.
+     */
+    public native static void setupRaSocket(FileDescriptor fd, int ifIndex) throws SocketException;
+
+    /**
      * Binds the current process to the network designated by {@code netId}.  All sockets created
      * in the future (and not explicitly bound via a bound {@link SocketFactory} (see
      * {@link Network#getSocketFactory}) will be bound to this network.  Note that if this
diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp
index f79254d..3e99521 100644
--- a/core/jni/android_net_NetUtils.cpp
+++ b/core/jni/android_net_NetUtils.cpp
@@ -44,28 +44,33 @@
 
 namespace android {
 
+static const uint32_t kEtherTypeOffset = offsetof(ether_header, ether_type);
+static const uint32_t kEtherHeaderLen = sizeof(ether_header);
+static const uint32_t kIPv4Protocol = kEtherHeaderLen + offsetof(iphdr, protocol);
+static const uint32_t kIPv4FlagsOffset = kEtherHeaderLen + offsetof(iphdr, frag_off);
+static const uint32_t kIPv6NextHeader = kEtherHeaderLen + offsetof(ip6_hdr, ip6_nxt);
+static const uint32_t kIPv6PayloadStart = kEtherHeaderLen + sizeof(ip6_hdr);
+static const uint32_t kICMPv6TypeOffset = kIPv6PayloadStart + offsetof(icmp6_hdr, icmp6_type);
+static const uint32_t kUDPSrcPortIndirectOffset = kEtherHeaderLen + offsetof(udphdr, source);
+static const uint32_t kUDPDstPortIndirectOffset = kEtherHeaderLen + offsetof(udphdr, dest);
 static const uint16_t kDhcpClientPort = 68;
 
 static void android_net_utils_attachDhcpFilter(JNIEnv *env, jobject clazz, jobject javaFd)
 {
-    uint32_t ip_offset = sizeof(ether_header);
-    uint32_t proto_offset = ip_offset + offsetof(iphdr, protocol);
-    uint32_t flags_offset = ip_offset + offsetof(iphdr, frag_off);
-    uint32_t dport_indirect_offset = ip_offset + offsetof(udphdr, dest);
     struct sock_filter filter_code[] = {
         // Check the protocol is UDP.
-        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  proto_offset),
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kIPv4Protocol),
         BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    IPPROTO_UDP, 0, 6),
 
         // Check this is not a fragment.
-        BPF_STMT(BPF_LD  | BPF_H    | BPF_ABS, flags_offset),
-        BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K,   0x1fff, 4, 0),
+        BPF_STMT(BPF_LD  | BPF_H    | BPF_ABS, kIPv4FlagsOffset),
+        BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K,   IP_OFFMASK, 4, 0),
 
         // Get the IP header length.
-        BPF_STMT(BPF_LDX | BPF_B    | BPF_MSH, ip_offset),
+        BPF_STMT(BPF_LDX | BPF_B    | BPF_MSH, kEtherHeaderLen),
 
         // Check the destination port.
-        BPF_STMT(BPF_LD  | BPF_H    | BPF_IND, dport_indirect_offset),
+        BPF_STMT(BPF_LD  | BPF_H    | BPF_IND, kUDPDstPortIndirectOffset),
         BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   kDhcpClientPort, 0, 1),
 
         // Accept or reject.
@@ -93,17 +98,13 @@
         return;
     }
 
-    uint32_t ipv6_offset = sizeof(ether_header);
-    uint32_t ipv6_next_header_offset = ipv6_offset + offsetof(ip6_hdr, ip6_nxt);
-    uint32_t icmp6_offset = ipv6_offset + sizeof(ip6_hdr);
-    uint32_t icmp6_type_offset = icmp6_offset + offsetof(icmp6_hdr, icmp6_type);
     struct sock_filter filter_code[] = {
         // Check IPv6 Next Header is ICMPv6.
-        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  ipv6_next_header_offset),
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kIPv6NextHeader),
         BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    IPPROTO_ICMPV6, 0, 3),
 
         // Check ICMPv6 type is Router Advertisement.
-        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  icmp6_type_offset),
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kICMPv6TypeOffset),
         BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    ND_ROUTER_ADVERT, 0, 1),
 
         // Accept or reject.
@@ -122,6 +123,174 @@
     }
 }
 
+// TODO: Move all this filter code into libnetutils.
+static void android_net_utils_attachControlPacketFilter(
+        JNIEnv *env, jobject clazz, jobject javaFd, jint hardwareAddressType) {
+    if (hardwareAddressType != ARPHRD_ETHER) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "attachControlPacketFilter only supports ARPHRD_ETHER");
+        return;
+    }
+
+    // Capture all:
+    //     - ARPs
+    //     - DHCPv4 packets
+    //     - Router Advertisements & Solicitations
+    //     - Neighbor Advertisements & Solicitations
+    //
+    // tcpdump:
+    //     arp or
+    //     '(ip and udp port 68)' or
+    //     '(icmp6 and ip6[40] >= 133 and ip6[40] <= 136)'
+    struct sock_filter filter_code[] = {
+        // Load the link layer next payload field.
+        BPF_STMT(BPF_LD  | BPF_H   | BPF_ABS,  kEtherTypeOffset),
+
+        // Accept all ARP.
+        // TODO: Figure out how to better filter ARPs on noisy networks.
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ETHERTYPE_ARP, 16, 0),
+
+        // If IPv4:
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ETHERTYPE_IP, 0, 9),
+
+        // Check the protocol is UDP.
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kIPv4Protocol),
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    IPPROTO_UDP, 0, 14),
+
+        // Check this is not a fragment.
+        BPF_STMT(BPF_LD  | BPF_H    | BPF_ABS, kIPv4FlagsOffset),
+        BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K,   IP_OFFMASK, 12, 0),
+
+        // Get the IP header length.
+        BPF_STMT(BPF_LDX | BPF_B    | BPF_MSH, kEtherHeaderLen),
+
+        // Check the source port.
+        BPF_STMT(BPF_LD  | BPF_H    | BPF_IND, kUDPSrcPortIndirectOffset),
+        BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   kDhcpClientPort, 8, 0),
+
+        // Check the destination port.
+        BPF_STMT(BPF_LD  | BPF_H    | BPF_IND, kUDPDstPortIndirectOffset),
+        BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   kDhcpClientPort, 6, 7),
+
+        // IPv6 ...
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ETHERTYPE_IPV6, 0, 6),
+        // ... check IPv6 Next Header is ICMPv6 (ignore fragments), ...
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kIPv6NextHeader),
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    IPPROTO_ICMPV6, 0, 4),
+        // ... and check the ICMPv6 type is one of RS/RA/NS/NA.
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kICMPv6TypeOffset),
+        BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K,    ND_ROUTER_SOLICIT, 0, 2),
+        BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K,    ND_NEIGHBOR_ADVERT, 1, 0),
+
+        // Accept or reject.
+        BPF_STMT(BPF_RET | BPF_K,              0xffff),
+        BPF_STMT(BPF_RET | BPF_K,              0)
+    };
+    struct sock_fprog filter = {
+        sizeof(filter_code) / sizeof(filter_code[0]),
+        filter_code,
+    };
+
+    int fd = jniGetFDFromFileDescriptor(env, javaFd);
+    if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
+    }
+}
+
+static void android_net_utils_setupRaSocket(JNIEnv *env, jobject clazz, jobject javaFd,
+        jint ifIndex)
+{
+    static const int kLinkLocalHopLimit = 255;
+
+    int fd = jniGetFDFromFileDescriptor(env, javaFd);
+
+    // Set an ICMPv6 filter that only passes Router Solicitations.
+    struct icmp6_filter rs_only;
+    ICMP6_FILTER_SETBLOCKALL(&rs_only);
+    ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &rs_only);
+    socklen_t len = sizeof(rs_only);
+    if (setsockopt(fd, IPPROTO_ICMPV6, ICMP6_FILTER, &rs_only, len) != 0) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "setsockopt(ICMP6_FILTER): %s", strerror(errno));
+        return;
+    }
+
+    // Most/all of the rest of these options can be set via Java code, but
+    // because we're here on account of setting an icmp6_filter go ahead
+    // and do it all natively for now.
+    //
+    // TODO: Consider moving these out to Java.
+
+    // Set the multicast hoplimit to 255 (link-local only).
+    int hops = kLinkLocalHopLimit;
+    len = sizeof(hops);
+    if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &hops, len) != 0) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "setsockopt(IPV6_MULTICAST_HOPS): %s", strerror(errno));
+        return;
+    }
+
+    // Set the unicast hoplimit to 255 (link-local only).
+    hops = kLinkLocalHopLimit;
+    len = sizeof(hops);
+    if (setsockopt(fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &hops, len) != 0) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "setsockopt(IPV6_UNICAST_HOPS): %s", strerror(errno));
+        return;
+    }
+
+    // Explicitly disable multicast loopback.
+    int off = 0;
+    len = sizeof(off);
+    if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &off, len) != 0) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "setsockopt(IPV6_MULTICAST_LOOP): %s", strerror(errno));
+        return;
+    }
+
+    // Specify the IPv6 interface to use for outbound multicast.
+    len = sizeof(ifIndex);
+    if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifIndex, len) != 0) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "setsockopt(IPV6_MULTICAST_IF): %s", strerror(errno));
+        return;
+    }
+
+    // Additional options to be considered:
+    //     - IPV6_TCLASS
+    //     - IPV6_RECVPKTINFO
+    //     - IPV6_RECVHOPLIMIT
+
+    // Bind to [::].
+    const struct sockaddr_in6 sin6 = {
+            .sin6_family = AF_INET6,
+            .sin6_port = 0,
+            .sin6_flowinfo = 0,
+            .sin6_addr = IN6ADDR_ANY_INIT,
+            .sin6_scope_id = 0,
+    };
+    auto sa = reinterpret_cast<const struct sockaddr *>(&sin6);
+    len = sizeof(sin6);
+    if (bind(fd, sa, len) != 0) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "bind(IN6ADDR_ANY): %s", strerror(errno));
+        return;
+    }
+
+    // Join the all-routers multicast group, ff02::2%index.
+    struct ipv6_mreq all_rtrs = {
+        .ipv6mr_multiaddr = {{{0xff,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2}}},
+        .ipv6mr_interface = ifIndex,
+    };
+    len = sizeof(all_rtrs);
+    if (setsockopt(fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &all_rtrs, len) != 0) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "setsockopt(IPV6_JOIN_GROUP): %s", strerror(errno));
+        return;
+    }
+}
+
 static jboolean android_net_utils_bindProcessToNetwork(JNIEnv *env, jobject thiz, jint netId)
 {
     return (jboolean) !setNetworkForProcess(netId);
@@ -170,6 +339,8 @@
     { "queryUserAccess", "(II)Z", (void*)android_net_utils_queryUserAccess },
     { "attachDhcpFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_attachDhcpFilter },
     { "attachRaFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachRaFilter },
+    { "attachControlPacketFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachControlPacketFilter },
+    { "setupRaSocket", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_setupRaSocket },
 };
 
 int register_android_net_NetworkUtils(JNIEnv* env)
diff --git a/core/tests/coretests/src/android/net/IpPrefixTest.java b/core/tests/coretests/src/android/net/IpPrefixTest.java
index fcc6389..4f2387d 100644
--- a/core/tests/coretests/src/android/net/IpPrefixTest.java
+++ b/core/tests/coretests/src/android/net/IpPrefixTest.java
@@ -18,14 +18,14 @@
 
 import android.net.IpPrefix;
 import android.os.Parcel;
-import static android.test.MoreAsserts.assertNotEqual;
 import android.test.suitebuilder.annotation.SmallTest;
-
-import static org.junit.Assert.assertArrayEquals;
 import java.net.InetAddress;
 import java.util.Random;
 import junit.framework.TestCase;
 
+import static android.test.MoreAsserts.assertNotEqual;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
 
 public class IpPrefixTest extends TestCase {
 
@@ -242,25 +242,42 @@
 
     @SmallTest
     public void testHashCode() {
-        IpPrefix p;
-        int oldCode = -1;
+        IpPrefix p = new IpPrefix(new byte[4], 0);
         Random random = new Random();
         for (int i = 0; i < 100; i++) {
+            final IpPrefix oldP = p;
             if (random.nextBoolean()) {
                 // IPv4.
                 byte[] b = new byte[4];
                 random.nextBytes(b);
                 p = new IpPrefix(b, random.nextInt(33));
-                assertNotEqual(oldCode, p.hashCode());
-                oldCode = p.hashCode();
             } else {
                 // IPv6.
                 byte[] b = new byte[16];
                 random.nextBytes(b);
                 p = new IpPrefix(b, random.nextInt(129));
-                assertNotEqual(oldCode, p.hashCode());
-                oldCode = p.hashCode();
             }
+            if (p.equals(oldP)) {
+              assertEquals(p.hashCode(), oldP.hashCode());
+            }
+            if (p.hashCode() != oldP.hashCode()) {
+              assertNotEqual(p, oldP);
+            }
+        }
+    }
+
+    @SmallTest
+    public void testHashCodeIsNotConstant() {
+        IpPrefix[] prefixes = {
+            new IpPrefix("2001:db8:f00::ace:d00d/127"),
+            new IpPrefix("192.0.2.0/23"),
+            new IpPrefix("::/0"),
+            new IpPrefix("0.0.0.0/0"),
+        };
+        for (int i = 0; i < prefixes.length; i++) {
+          for (int j = i + 1; j < prefixes.length; j++) {
+            assertNotEqual(prefixes[i].hashCode(), prefixes[j].hashCode());
+          }
         }
     }
 
diff --git a/core/tests/coretests/src/android/net/NetworkStatsTest.java b/core/tests/coretests/src/android/net/NetworkStatsTest.java
index 9074f8a..eb85eb4 100644
--- a/core/tests/coretests/src/android/net/NetworkStatsTest.java
+++ b/core/tests/coretests/src/android/net/NetworkStatsTest.java
@@ -16,6 +16,9 @@
 
 package android.net;
 
+import static android.net.NetworkStats.METERED_ALL;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.METERED_YES;
 import static android.net.NetworkStats.ROAMING_ALL;
 import static android.net.NetworkStats.ROAMING_NO;
 import static android.net.NetworkStats.ROAMING_YES;
@@ -45,103 +48,124 @@
     private static final long TEST_START = 1194220800000L;
 
     public void testFindIndex() throws Exception {
-        final NetworkStats stats = new NetworkStats(TEST_START, 4)
-                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, ROAMING_NO, 1024L, 8L, 0L,
-                        0L, 10)
-                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, ROAMING_NO, 0L, 0L, 1024L,
-                        8L, 11)
-                .addValues(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, ROAMING_NO, 1024L, 8L,
-                        1024L, 8L, 12)
-                .addValues(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, ROAMING_YES, 1024L, 8L,
-                        1024L, 8L, 12);
+        final NetworkStats stats = new NetworkStats(TEST_START, 5)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 1024L,
+                        8L, 0L, 0L, 10)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 0L, 0L,
+                        1024L, 8L, 11)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, 0L, 0L,
+                        1024L, 8L, 11)
+                .addValues(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 1024L,
+                        8L, 1024L, 8L, 12)
+                .addValues(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES, 1024L,
+                        8L, 1024L, 8L, 12);
 
-        assertEquals(3, stats.findIndex(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, ROAMING_YES));
-        assertEquals(2, stats.findIndex(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, ROAMING_NO));
-        assertEquals(1, stats.findIndex(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, ROAMING_NO));
-        assertEquals(0, stats.findIndex(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, ROAMING_NO));
-        assertEquals(-1, stats.findIndex(TEST_IFACE, 6, SET_DEFAULT, TAG_NONE, ROAMING_NO));
+        assertEquals(4, stats.findIndex(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_YES,
+                ROAMING_YES));
+        assertEquals(3, stats.findIndex(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO));
+        assertEquals(2, stats.findIndex(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES,
+                ROAMING_NO));
+        assertEquals(1, stats.findIndex(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO));
+        assertEquals(0, stats.findIndex(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO));
+        assertEquals(-1, stats.findIndex(TEST_IFACE, 6, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO));
     }
 
     public void testFindIndexHinted() {
         final NetworkStats stats = new NetworkStats(TEST_START, 3)
-                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, ROAMING_NO, 1024L, 8L, 0L,
-                        0L, 10)
-                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, ROAMING_NO, 0L, 0L, 1024L,
-                        8L, 11)
-                .addValues(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, ROAMING_NO, 1024L, 8L,
-                        1024L, 8L, 12)
-                .addValues(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, ROAMING_NO, 1024L, 8L,
-                        0L, 0L, 10)
-                .addValues(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, ROAMING_NO, 0L, 0L, 1024L,
-                        8L, 11)
-                .addValues(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, ROAMING_NO, 1024L, 8L,
-                        1024L, 8L, 12)
-                .addValues(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, ROAMING_YES, 1024L, 8L,
-                        1024L, 8L, 12);
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 1024L,
+                        8L, 0L, 0L, 10)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 0L, 0L,
+                        1024L, 8L, 11)
+                .addValues(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 1024L,
+                        8L, 1024L, 8L, 12)
+                .addValues(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                        1024L, 8L, 0L, 0L, 10)
+                .addValues(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, 0L, 0L,
+                        1024L, 8L, 11)
+                .addValues(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO, 0L, 0L,
+                        1024L, 8L, 11)
+                .addValues(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 1024L,
+                        8L, 1024L, 8L, 12)
+                .addValues(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES, 1024L,
+                        8L, 1024L, 8L, 12);
 
         // verify that we correctly find across regardless of hinting
         for (int hint = 0; hint < stats.size(); hint++) {
             assertEquals(0, stats.findIndexHinted(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE,
-                    ROAMING_NO, hint));
+                    METERED_NO, ROAMING_NO, hint));
             assertEquals(1, stats.findIndexHinted(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE,
-                    ROAMING_NO, hint));
+                    METERED_NO, ROAMING_NO, hint));
             assertEquals(2, stats.findIndexHinted(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE,
-                    ROAMING_NO, hint));
+                    METERED_NO, ROAMING_NO, hint));
             assertEquals(3, stats.findIndexHinted(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE,
-                    ROAMING_NO, hint));
+                    METERED_NO, ROAMING_NO, hint));
             assertEquals(4, stats.findIndexHinted(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D,
-                    ROAMING_NO, hint));
-            assertEquals(5, stats.findIndexHinted(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE,
-                    ROAMING_NO, hint));
+                    METERED_NO, ROAMING_NO, hint));
+            assertEquals(5, stats.findIndexHinted(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D,
+                    METERED_YES, ROAMING_NO, hint));
             assertEquals(6, stats.findIndexHinted(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE,
-                    ROAMING_YES, hint));
+                    METERED_NO, ROAMING_NO, hint));
+            assertEquals(7, stats.findIndexHinted(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE,
+                    METERED_YES, ROAMING_YES, hint));
             assertEquals(-1, stats.findIndexHinted(TEST_IFACE, 6, SET_DEFAULT, TAG_NONE,
-                    ROAMING_NO, hint));
+                    METERED_NO, ROAMING_NO, hint));
         }
     }
 
     public void testAddEntryGrow() throws Exception {
-        final NetworkStats stats = new NetworkStats(TEST_START, 3);
+        final NetworkStats stats = new NetworkStats(TEST_START, 4);
 
         assertEquals(0, stats.size());
-        assertEquals(3, stats.internalSize());
+        assertEquals(4, stats.internalSize());
 
-        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, ROAMING_NO, 1L, 1L, 2L,
-                2L, 3);
-        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, ROAMING_NO, 2L, 2L, 2L,
-                2L, 4);
-        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, ROAMING_YES, 3L, 3L, 2L,
-                2L, 5);
-
-        assertEquals(3, stats.size());
-        assertEquals(3, stats.internalSize());
-
-        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, ROAMING_NO, 4L, 40L, 4L,
-                40L, 7);
-        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, ROAMING_NO, 5L, 50L, 4L,
-                40L, 8);
-        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, ROAMING_NO, 6L, 60L, 5L,
-                50L, 10);
-        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, ROAMING_YES, 7L, 70L, 5L,
-                50L, 11);
-
-        assertEquals(7, stats.size());
-        assertTrue(stats.internalSize() >= 7);
-
-        assertValues(stats, 0, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, ROAMING_NO, 1L, 1L,
+        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 1L, 1L,
                 2L, 2L, 3);
-        assertValues(stats, 1, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, ROAMING_NO, 2L, 2L,
+        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 2L, 2L,
                 2L, 2L, 4);
-        assertValues(stats, 2, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, ROAMING_YES, 3L, 3L,
-                2L, 2L, 5);
-        assertValues(stats, 3, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, ROAMING_NO, 4L,
+        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, 3L,
+                3L, 2L, 2L, 5);
+        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES, 3L,
+                3L, 2L, 2L, 5);
+
+        assertEquals(4, stats.size());
+        assertEquals(4, stats.internalSize());
+
+        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 4L,
                 40L, 4L, 40L, 7);
-        assertValues(stats, 4, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, ROAMING_NO, 5L,
+        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 5L,
                 50L, 4L, 40L, 8);
-        assertValues(stats, 5, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, ROAMING_NO, 6L,
+        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 6L,
                 60L, 5L, 50L, 10);
-        assertValues(stats, 6, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, ROAMING_YES, 7L,
+        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, 7L,
                 70L, 5L, 50L, 11);
+        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES, 7L,
+                70L, 5L, 50L, 11);
+
+        assertEquals(9, stats.size());
+        assertTrue(stats.internalSize() >= 9);
+
+        assertValues(stats, 0, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                1L, 1L, 2L, 2L, 3);
+        assertValues(stats, 1, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                2L, 2L, 2L, 2L, 4);
+        assertValues(stats, 2, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES,
+                3L, 3L, 2L, 2L, 5);
+        assertValues(stats, 3, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES,
+                ROAMING_YES, 3L, 3L, 2L, 2L, 5);
+        assertValues(stats, 4, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                4L, 40L, 4L, 40L, 7);
+        assertValues(stats, 5, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                5L, 50L, 4L, 40L, 8);
+        assertValues(stats, 6, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                6L, 60L, 5L, 50L, 10);
+        assertValues(stats, 7, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES,
+                7L, 70L, 5L, 50L, 11);
+        assertValues(stats, 8, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES,
+                ROAMING_YES, 7L, 70L, 5L, 50L, 11);
     }
 
     public void testCombineExisting() throws Exception {
@@ -152,20 +176,18 @@
         stats.combineValues(TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, -128L, -1L,
                 -128L, -1L, -1);
 
-        assertValues(stats, 0, TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, ROAMING_NO, 384L, 3L,
-                128L, 1L, 9);
-        assertValues(stats, 1, TEST_IFACE, 1001, SET_DEFAULT, 0xff, ROAMING_NO, 128L, 1L, 128L,
-                1L, 2);
+        assertValues(stats, 0, TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                384L, 3L, 128L, 1L, 9);
+        assertValues(stats, 1, TEST_IFACE, 1001, SET_DEFAULT, 0xff, METERED_NO, ROAMING_NO, 128L,
+                1L, 128L, 1L, 2);
 
         // now try combining that should create row
-        stats.combineValues(TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, 128L, 1L,
-                128L, 1L, 3);
-        assertValues(stats, 2, TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, ROAMING_NO, 128L, 1L,
-                128L, 1L, 3);
-        stats.combineValues(TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, 128L, 1L,
-                128L, 1L, 3);
-        assertValues(stats, 2, TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, ROAMING_NO, 256L, 2L,
-                256L, 2L, 6);
+        stats.combineValues(TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 3);
+        assertValues(stats, 2, TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                128L, 1L, 128L, 1L, 3);
+        stats.combineValues(TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 3);
+        assertValues(stats, 2, TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                256L, 2L, 256L, 2L, 6);
     }
 
     public void testSubtractIdenticalData() throws Exception {
@@ -180,10 +202,10 @@
         final NetworkStats result = after.subtract(before);
 
         // identical data should result in zero delta
-        assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, ROAMING_NO, 0L, 0L, 0L,
-                0L, 0);
-        assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, ROAMING_NO, 0L, 0L, 0L,
-                0L, 0);
+        assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 0L,
+                0L, 0L, 0L, 0);
+        assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 0L,
+                0L, 0L, 0L, 0);
     }
 
     public void testSubtractIdenticalRows() throws Exception {
@@ -198,10 +220,10 @@
         final NetworkStats result = after.subtract(before);
 
         // expect delta between measurements
-        assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, ROAMING_NO, 1L, 1L, 2L,
-                1L, 4);
-        assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, ROAMING_NO, 3L, 1L, 4L,
-                1L, 8);
+        assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 1L,
+                1L, 2L, 1L, 4);
+        assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 3L,
+                1L, 4L, 1L, 8);
     }
 
     public void testSubtractNewRows() throws Exception {
@@ -217,12 +239,12 @@
         final NetworkStats result = after.subtract(before);
 
         // its okay to have new rows
-        assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, ROAMING_NO, 0L, 0L, 0L,
-                0L, 0);
-        assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, ROAMING_NO, 0L, 0L, 0L,
-                0L, 0);
-        assertValues(result, 2, TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, ROAMING_NO, 1024L, 8L,
-                1024L, 8L, 20);
+        assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 0L,
+                0L, 0L, 0L, 0);
+        assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 0L,
+                0L, 0L, 0L, 0);
+        assertValues(result, 2, TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                1024L, 8L, 1024L, 8L, 20);
     }
 
     public void testSubtractMissingRows() throws Exception {
@@ -237,8 +259,8 @@
 
         // should silently drop omitted rows
         assertEquals(1, result.size());
-        assertValues(result, 0, TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, ROAMING_NO, 1L,
-                2L, 3L, 4L, 0);
+        assertValues(result, 0, TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, 1L, 2L, 3L, 4L, 0);
         assertEquals(4L, result.getTotalBytes());
     }
 
@@ -263,13 +285,22 @@
                 .addValues(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 8L, 0L, 0L, 0L, 0L);
         assertEquals(64L, uidTag.getTotalBytes());
 
+        final NetworkStats uidMetered = new NetworkStats(TEST_START, 3)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 32L, 0L,
+                        0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, 32L, 0L,
+                        0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, 32L, 0L,
+                        0L, 0L, 0L);
+        assertEquals(96L, uidMetered.getTotalBytes());
+
         final NetworkStats uidRoaming = new NetworkStats(TEST_START, 3)
-                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, ROAMING_NO, 32L, 0L, 0L, 0L,
-                        0L)
-                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, ROAMING_NO, 32L, 0L, 0L, 0L,
-                        0L)
-                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, ROAMING_YES, 32L, 0L, 0L, 0L,
-                        0L);
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 32L, 0L,
+                        0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, 32L, 0L,
+                        0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, 32L, 0L,
+                        0L, 0L, 0L);
         assertEquals(96L, uidRoaming.getTotalBytes());
     }
 
@@ -283,95 +314,95 @@
 
     public void testGroupedByIfaceAll() throws Exception {
         final NetworkStats uidStats = new NetworkStats(TEST_START, 3)
-                .addValues(IFACE_ALL, 100, SET_ALL, TAG_NONE, ROAMING_NO, 128L, 8L, 0L, 2L,
-                        20L)
-                .addValues(IFACE_ALL, 101, SET_FOREGROUND, TAG_NONE, ROAMING_NO, 128L, 8L, 0L,
+                .addValues(IFACE_ALL, 100, SET_ALL, TAG_NONE, METERED_NO, ROAMING_NO, 128L, 8L, 0L,
                         2L, 20L)
-                .addValues(IFACE_ALL, 101, SET_ALL, TAG_NONE, ROAMING_YES, 128L, 8L, 0L, 2L,
-                        20L);
+                .addValues(IFACE_ALL, 101, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_NO, 128L,
+                        8L, 0L, 2L, 20L)
+                .addValues(IFACE_ALL, 101, SET_ALL, TAG_NONE, METERED_NO, ROAMING_YES, 128L, 8L, 0L,
+                        2L, 20L);
         final NetworkStats grouped = uidStats.groupedByIface();
 
         assertEquals(3, uidStats.size());
         assertEquals(1, grouped.size());
 
-        assertValues(grouped, 0, IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, ROAMING_ALL, 384L, 24L, 0L,
-                6L, 0L);
+        assertValues(grouped, 0, IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                384L, 24L, 0L, 6L, 0L);
     }
 
     public void testGroupedByIface() throws Exception {
         final NetworkStats uidStats = new NetworkStats(TEST_START, 7)
-                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, ROAMING_NO, 128L, 8L, 0L,
-                        2L, 20L)
-                .addValues(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, ROAMING_NO, 512L, 32L, 0L,
-                        0L, 0L)
-                .addValues(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, ROAMING_NO, 64L, 4L, 0L, 0L,
-                        0L)
-                .addValues(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, ROAMING_NO, 512L, 32L,
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 128L, 8L,
+                        0L, 2L, 20L)
+                .addValues(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 512L,
+                        32L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, 64L, 4L,
                         0L, 0L, 0L)
-                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, ROAMING_NO, 128L, 8L, 0L,
-                        0L, 0L)
-                .addValues(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, ROAMING_NO, 128L, 8L, 0L, 0L,
-                        0L)
-                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, ROAMING_YES, 128L, 8L, 0L,
-                        0L, 0L);
+                .addValues(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, 512L,
+                        32L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 128L, 8L,
+                        0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO, 128L, 8L,
+                        0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, 128L,
+                        8L, 0L, 0L, 0L);
 
         final NetworkStats grouped = uidStats.groupedByIface();
 
         assertEquals(7, uidStats.size());
 
         assertEquals(2, grouped.size());
-        assertValues(grouped, 0, TEST_IFACE, UID_ALL, SET_ALL, TAG_NONE, ROAMING_ALL, 384L, 24L, 0L,
-                2L, 0L);
-        assertValues(grouped, 1, TEST_IFACE2, UID_ALL, SET_ALL, TAG_NONE, ROAMING_ALL, 1024L, 64L,
-                0L, 0L, 0L);
+        assertValues(grouped, 0, TEST_IFACE, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                384L, 24L, 0L, 2L, 0L);
+        assertValues(grouped, 1, TEST_IFACE2, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                1024L, 64L, 0L, 0L, 0L);
     }
 
     public void testAddAllValues() {
         final NetworkStats first = new NetworkStats(TEST_START, 5)
-                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, ROAMING_NO, 32L, 0L, 0L, 0L,
-                        0L)
-                .addValues(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, ROAMING_NO, 32L, 0L, 0L,
-                        0L, 0L)
-                .addValues(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, ROAMING_YES, 32L, 0L, 0L,
-                        0L, 0L);
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, 32L, 0L,
+                        0L, 0L, 0L)
+                .addValues(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, 32L,
+                        0L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_YES, 32L,
+                        0L, 0L, 0L, 0L);
 
         final NetworkStats second = new NetworkStats(TEST_START, 2)
-                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, ROAMING_NO, 32L, 0L, 0L, 0L,
-                        0L)
-                .addValues(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, ROAMING_NO, 32L, 0L,
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, 32L, 0L,
                         0L, 0L, 0L)
-                .addValues(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, ROAMING_YES, 32L, 0L, 0L,
-                        0L, 0L);
+                .addValues(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 32L,
+                        0L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_YES, 32L,
+                        0L, 0L, 0L, 0L);
 
         first.combineAllValues(second);
 
         assertEquals(4, first.size());
-        assertValues(first, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, ROAMING_NO, 64L, 0L, 0L,
-                0L, 0L);
-        assertValues(first, 1, TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, ROAMING_NO, 32L, 0L,
-                0L, 0L, 0L);
-        assertValues(first, 2, TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, ROAMING_YES, 64L, 0L,
-                0L, 0L, 0L);
-        assertValues(first, 3, TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, ROAMING_NO, 32L,
+        assertValues(first, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, 64L,
                 0L, 0L, 0L, 0L);
+        assertValues(first, 1, TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                32L, 0L, 0L, 0L, 0L);
+        assertValues(first, 2, TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_YES,
+                64L, 0L, 0L, 0L, 0L);
+        assertValues(first, 3, TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                32L, 0L, 0L, 0L, 0L);
     }
 
     public void testGetTotal() {
         final NetworkStats stats = new NetworkStats(TEST_START, 7)
-                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, ROAMING_NO, 128L, 8L, 0L,
-                        2L, 20L)
-                .addValues(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, ROAMING_NO, 512L, 32L, 0L,
-                        0L, 0L)
-                .addValues(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, ROAMING_NO, 64L, 4L, 0L, 0L,
-                        0L)
-                .addValues(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, ROAMING_NO, 512L, 32L,
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 128L, 8L,
+                        0L, 2L, 20L)
+                .addValues(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 512L,
+                        32L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, 64L, 4L,
                         0L, 0L, 0L)
-                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, ROAMING_NO, 128L, 8L, 0L,
-                        0L, 0L)
-                .addValues(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, ROAMING_NO, 128L, 8L, 0L, 0L,
-                        0L)
-                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, ROAMING_YES, 128L, 8L, 0L,
-                        0L, 0L);
+                .addValues(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, 512L,
+                        32L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, 128L,
+                        8L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, 128L, 8L,
+                        0L, 0L, 0L)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, 128L,
+                        8L, 0L, 0L, 0L);
 
         assertValues(stats.getTotal(null), 1408L, 88L, 0L, 2L, 20L);
         assertValues(stats.getTotal(null, 100), 1280L, 80L, 0L, 2L, 20L);
@@ -396,10 +427,10 @@
         final NetworkStats after = before.withoutUids(new int[] { 100 });
         assertEquals(6, before.size());
         assertEquals(2, after.size());
-        assertValues(after, 0, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, ROAMING_NO, 128L, 8L,
-                0L, 0L, 0L);
-        assertValues(after, 1, TEST_IFACE, 101, SET_DEFAULT, 0xF00D, ROAMING_NO, 128L, 8L, 0L,
-                0L, 0L);
+        assertValues(after, 0, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                128L, 8L, 0L, 0L, 0L);
+        assertValues(after, 1, TEST_IFACE, 101, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, 128L,
+                8L, 0L, 0L, 0L);
     }
 
     public void testClone() throws Exception {
@@ -434,105 +465,175 @@
         final String underlyingIface = "wlan0";
         final int testTag1 = 8888;
         NetworkStats delta = new NetworkStats(TEST_START, 17)
-            .addValues(tunIface, 10100, SET_DEFAULT, TAG_NONE, 39605L, 46L, 12259L, 55L, 0L)
-            .addValues(tunIface, 10100, SET_FOREGROUND, TAG_NONE, 0L, 0L, 0L, 0L, 0L)
-            .addValues(tunIface, 10120, SET_DEFAULT, TAG_NONE, 72667L, 197L, 43909L, 241L, 0L)
-            .addValues(tunIface, 10120, SET_FOREGROUND, TAG_NONE, 9297L, 17L, 4128L, 21L, 0L)
+            .addValues(tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 39605L, 46L,
+                    12259L, 55L, 0L)
+            .addValues(tunIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, 0L, 0L,
+                    0L, 0L, 0L)
+            .addValues(tunIface, 10120, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 72667L, 197L,
+                    43909L, 241L, 0L)
+            .addValues(tunIface, 10120, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, 9297L,
+                    17L, 4128L, 21L, 0L)
             // VPN package also uses some traffic through unprotected network.
-            .addValues(tunIface, tunUid, SET_DEFAULT, TAG_NONE, 4983L, 10L, 1801L, 12L, 0L)
-            .addValues(tunIface, tunUid, SET_FOREGROUND, TAG_NONE, 0L, 0L, 0L, 0L, 0L)
+            .addValues(tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 4983L, 10L,
+                    1801L, 12L, 0L)
+            .addValues(tunIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, 0L, 0L,
+                    0L, 0L, 0L)
             // Tag entries
-            .addValues(tunIface, 10120, SET_DEFAULT, testTag1, 21691L, 41L, 13820L, 51L, 0L)
-            .addValues(tunIface, 10120, SET_FOREGROUND, testTag1, 1281L, 2L, 665L, 2L, 0L)
+            .addValues(tunIface, 10120, SET_DEFAULT, testTag1, METERED_NO, ROAMING_NO, 21691L, 41L,
+                    13820L, 51L, 0L)
+            .addValues(tunIface, 10120, SET_FOREGROUND, testTag1, METERED_NO, ROAMING_NO, 1281L, 2L,
+                    665L, 2L, 0L)
             // Irrelevant entries
-            .addValues(TEST_IFACE, 10100, SET_DEFAULT, TAG_NONE, 1685L, 5L, 2070L, 6L, 0L)
+            .addValues(TEST_IFACE, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 1685L, 5L,
+                    2070L, 6L, 0L)
             // Underlying Iface entries
-            .addValues(underlyingIface, 10100, SET_DEFAULT, TAG_NONE, 5178L, 8L, 2139L, 11L, 0L)
-            .addValues(underlyingIface, 10100, SET_FOREGROUND, TAG_NONE, 0L, 0L, 0L, 0L, 0L)
-            .addValues(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, 149873L, 287L,
-                    59217L /* smaller than sum(tun0) */, 299L /* smaller than sum(tun0) */, 0L)
-            .addValues(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, 0L, 0L, 0L, 0L, 0L);
+            .addValues(underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 5178L,
+                    8L, 2139L, 11L, 0L)
+            .addValues(underlyingIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, 0L,
+                    0L, 0L, 0L, 0L)
+            .addValues(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                    149873L, 287L, 59217L /* smaller than sum(tun0) */,
+                    299L /* smaller than sum(tun0) */, 0L)
+            .addValues(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                    0L, 0L, 0L, 0L, 0L);
 
         assertTrue(delta.migrateTun(tunUid, tunIface, underlyingIface));
-        assertEquals(21, delta.size());
+        assertEquals(20, delta.size());
 
         // tunIface and TEST_IFACE entries are not changed.
-        assertValues(delta, 0, tunIface, 10100, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+        assertValues(delta, 0, tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                 39605L, 46L, 12259L, 55L, 0L);
-        assertValues(delta, 1, tunIface, 10100, SET_FOREGROUND, TAG_NONE, ROAMING_NO, 0L, 0L,
-                0L, 0L, 0L);
-        assertValues(delta, 2, tunIface, 10120, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+        assertValues(delta, 1, tunIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                0L, 0L, 0L, 0L, 0L);
+        assertValues(delta, 2, tunIface, 10120, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                 72667L, 197L, 43909L, 241L, 0L);
-        assertValues(delta, 3, tunIface, 10120, SET_FOREGROUND, TAG_NONE, ROAMING_NO,
+        assertValues(delta, 3, tunIface, 10120, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
                 9297L, 17L, 4128L, 21L, 0L);
-        assertValues(delta, 4, tunIface, tunUid, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+        assertValues(delta, 4, tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                 4983L, 10L, 1801L, 12L, 0L);
-        assertValues(delta, 5, tunIface, tunUid, SET_FOREGROUND, TAG_NONE, ROAMING_NO, 0L, 0L,
-                0L, 0L, 0L);
-        assertValues(delta, 6, tunIface, 10120, SET_DEFAULT, testTag1, ROAMING_NO,
+        assertValues(delta, 5, tunIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                0L, 0L, 0L, 0L, 0L);
+        assertValues(delta, 6, tunIface, 10120, SET_DEFAULT, testTag1, METERED_NO, ROAMING_NO,
                 21691L, 41L, 13820L, 51L, 0L);
-        assertValues(delta, 7, tunIface, 10120, SET_FOREGROUND, testTag1, ROAMING_NO, 1281L,
-                2L, 665L, 2L, 0L);
-        assertValues(delta, 8, TEST_IFACE, 10100, SET_DEFAULT, TAG_NONE, ROAMING_NO, 1685L, 5L,
-                2070L, 6L, 0L);
+        assertValues(delta, 7, tunIface, 10120, SET_FOREGROUND, testTag1, METERED_NO, ROAMING_NO,
+                1281L, 2L, 665L, 2L, 0L);
+        assertValues(delta, 8, TEST_IFACE, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                1685L, 5L, 2070L, 6L, 0L);
 
         // Existing underlying Iface entries are updated
-        assertValues(delta, 9, underlyingIface, 10100, SET_DEFAULT, TAG_NONE, ROAMING_NO,
-                44783L, 54L, 13829L, 60L, 0L);
-        assertValues(delta, 10, underlyingIface, 10100, SET_FOREGROUND, TAG_NONE, ROAMING_NO,
-                0L, 0L, 0L, 0L, 0L);
+        assertValues(delta, 9, underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, 44783L, 54L, 14178L, 62L, 0L);
+        assertValues(delta, 10, underlyingIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO,
+                ROAMING_NO, 0L, 0L, 0L, 0L, 0L);
 
         // VPN underlying Iface entries are updated
-        assertValues(delta, 11, underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, ROAMING_NO,
-                28304L, 27L, 1719L, 12L, 0L);
-        assertValues(delta, 12, underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, ROAMING_NO,
-                0L, 0L, 0L, 0L, 0L);
+        assertValues(delta, 11, underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, 28304L, 27L, 1L, 2L, 0L);
+        assertValues(delta, 12, underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO,
+                ROAMING_NO, 0L, 0L, 0L, 0L, 0L);
 
         // New entries are added for new application's underlying Iface traffic
-        assertContains(delta, underlyingIface, 10120, SET_DEFAULT, TAG_NONE, ROAMING_NO,
-                72667L, 197L, 41872L, 219L, 0L);
-        assertContains(delta, underlyingIface, 10120, SET_FOREGROUND, TAG_NONE, ROAMING_NO,
-                9297L, 17L, 3936, 19L, 0L);
-        assertContains(delta, underlyingIface, 10120, SET_DEFAULT, testTag1, ROAMING_NO,
-                21691L, 41L, 13179L, 46L, 0L);
-        assertContains(delta, underlyingIface, 10120, SET_FOREGROUND, testTag1, ROAMING_NO,
-                1281L, 2L, 634L, 1L, 0L);
+        assertContains(delta, underlyingIface, 10120, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, 72667L, 197L, 43123L, 227L, 0L);
+        assertContains(delta, underlyingIface, 10120, SET_FOREGROUND, TAG_NONE, METERED_NO,
+                ROAMING_NO, 9297L, 17L, 4054, 19L, 0L);
+        assertContains(delta, underlyingIface, 10120, SET_DEFAULT, testTag1, METERED_NO,
+                ROAMING_NO, 21691L, 41L, 13572L, 48L, 0L);
+        assertContains(delta, underlyingIface, 10120, SET_FOREGROUND, testTag1, METERED_NO,
+                ROAMING_NO, 1281L, 2L, 653L, 1L, 0L);
 
         // New entries are added for debug purpose
-        assertContains(delta, underlyingIface, 10100, SET_DBG_VPN_IN, TAG_NONE, ROAMING_NO,
-                39605L, 46L, 11690, 49, 0);
-        assertContains(delta, underlyingIface, 10120, SET_DBG_VPN_IN, TAG_NONE, ROAMING_NO,
-                81964, 214, 45808, 238, 0);
-        assertContains(delta, underlyingIface, tunUid, SET_DBG_VPN_IN, TAG_NONE, ROAMING_NO,
-                4983, 10, 1717, 10, 0);
-        assertContains(delta, underlyingIface, tunUid, SET_DBG_VPN_OUT, TAG_NONE, ROAMING_ALL,
-                126552, 270, 59215, 297, 0);
+        assertContains(delta, underlyingIface, 10100, SET_DBG_VPN_IN, TAG_NONE, METERED_NO,
+                ROAMING_NO, 39605L, 46L, 12039, 51, 0);
+        assertContains(delta, underlyingIface, 10120, SET_DBG_VPN_IN, TAG_NONE, METERED_NO,
+                ROAMING_NO, 81964, 214, 47177, 246, 0);
+        assertContains(delta, underlyingIface, tunUid, SET_DBG_VPN_OUT, TAG_NONE, METERED_ALL,
+                ROAMING_ALL, 121569, 260, 59216, 297, 0);
 
     }
 
+    // Tests a case where all of the data received by the tun0 interface is echo back into the tun0
+    // interface by the vpn app before it's sent out of the underlying interface. The VPN app should
+    // not be charged for the echoed data but it should still be charged for any extra data it sends
+    // via the underlying interface.
+    public void testMigrateTun_VpnAsLoopback() {
+        final int tunUid = 10030;
+        final String tunIface = "tun0";
+        final String underlyingIface = "wlan0";
+        NetworkStats delta = new NetworkStats(TEST_START, 9)
+            // 2 different apps sent/receive data via tun0.
+            .addValues(tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 50000L, 25L,
+                    100000L, 50L, 0L)
+            .addValues(tunIface, 20100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 500L, 2L,
+                    200L, 5L, 0L)
+            // VPN package resends data through the tunnel (with exaggerated overhead)
+            .addValues(tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 240000,
+                    100L, 120000L, 60L, 0L)
+            // 1 app already has some traffic on the underlying interface, the other doesn't yet
+            .addValues(underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 1000L,
+                    10L, 2000L, 20L, 0L)
+            // Traffic through the underlying interface via the vpn app.
+            // This test should redistribute this data correctly.
+            .addValues(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                    75500L, 37L, 130000L, 70L, 0L);
+
+        assertTrue(delta.migrateTun(tunUid, tunIface, underlyingIface));
+        assertEquals(9, delta.size());
+
+        // tunIface entries should not be changed.
+        assertValues(delta, 0, tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                50000L, 25L, 100000L, 50L, 0L);
+        assertValues(delta, 1, tunIface, 20100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                500L, 2L, 200L, 5L, 0L);
+        assertValues(delta, 2, tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                240000L, 100L, 120000L, 60L, 0L);
+
+        // Existing underlying Iface entries are updated
+        assertValues(delta, 3, underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, 51000L, 35L, 102000L, 70L, 0L);
+
+        // VPN underlying Iface entries are updated
+        assertValues(delta, 4, underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, 25000L, 10L, 29800L, 15L, 0L);
+
+        // New entries are added for new application's underlying Iface traffic
+        assertContains(delta, underlyingIface, 20100, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, 500L, 2L, 200L, 5L, 0L);
+
+        // New entries are added for debug purpose
+        assertContains(delta, underlyingIface, 10100, SET_DBG_VPN_IN, TAG_NONE, METERED_NO,
+                ROAMING_NO, 50000L, 25L, 100000L, 50L, 0L);
+        assertContains(delta, underlyingIface, 20100, SET_DBG_VPN_IN, TAG_NONE, METERED_NO,
+                ROAMING_NO, 500, 2L, 200L, 5L, 0L);
+        assertContains(delta, underlyingIface, tunUid, SET_DBG_VPN_OUT, TAG_NONE, METERED_ALL,
+                ROAMING_ALL, 50500L, 27L, 100200L, 55, 0);
+    }
+
     private static void assertContains(NetworkStats stats,  String iface, int uid, int set,
-            int tag, int roaming, long rxBytes, long rxPackets, long txBytes, long txPackets,
-            long operations) {
-        int index = stats.findIndex(iface, uid, set, tag, roaming);
+            int tag, int metered, int roaming, long rxBytes, long rxPackets, long txBytes,
+            long txPackets, long operations) {
+        int index = stats.findIndex(iface, uid, set, tag, metered, roaming);
         assertTrue(index != -1);
-        assertValues(stats, index, iface, uid, set, tag, roaming,
+        assertValues(stats, index, iface, uid, set, tag, metered, roaming,
                 rxBytes, rxPackets, txBytes, txPackets, operations);
     }
 
     private static void assertValues(NetworkStats stats, int index, String iface, int uid, int set,
-            int tag, int roaming, long rxBytes, long rxPackets, long txBytes, long txPackets,
-            long operations) {
+            int tag, int metered, int roaming, long rxBytes, long rxPackets, long txBytes,
+            long txPackets, long operations) {
         final NetworkStats.Entry entry = stats.getValues(index, null);
-        assertValues(entry, iface, uid, set, tag, roaming);
+        assertValues(entry, iface, uid, set, tag, metered, roaming);
         assertValues(entry, rxBytes, rxPackets, txBytes, txPackets, operations);
     }
 
     private static void assertValues(
-            NetworkStats.Entry entry, String iface, int uid, int set, int tag, int roaming) {
+            NetworkStats.Entry entry, String iface, int uid, int set, int tag, int metered,
+            int roaming) {
         assertEquals(iface, entry.iface);
         assertEquals(uid, entry.uid);
         assertEquals(set, entry.set);
         assertEquals(tag, entry.tag);
+        assertEquals(metered, entry.metered);
         assertEquals(roaming, entry.roaming);
     }
 
diff --git a/core/tests/coretests/src/com/android/internal/net/NetworkStatsFactoryTest.java b/core/tests/coretests/src/com/android/internal/net/NetworkStatsFactoryTest.java
index 327f3fd..7f13abc 100644
--- a/core/tests/coretests/src/com/android/internal/net/NetworkStatsFactoryTest.java
+++ b/core/tests/coretests/src/com/android/internal/net/NetworkStatsFactoryTest.java
@@ -16,6 +16,7 @@
 
 package com.android.internal.net;
 
+import static android.net.NetworkStats.METERED_NO;
 import static android.net.NetworkStats.ROAMING_NO;
 import static android.net.NetworkStats.SET_ALL;
 import static android.net.NetworkStats.SET_DEFAULT;
@@ -101,12 +102,14 @@
 
         final NetworkStats stats = mFactory.readNetworkStatsDetail();
         assertEquals(70, stats.size());
-        assertStatsEntry(stats, "rmnet1", 10021, SET_DEFAULT, 0x30100000, 219110L, 578L, 227423L, 676L);
+        assertStatsEntry(stats, "rmnet1", 10021, SET_DEFAULT, 0x30100000, 219110L, 578L, 227423L,
+                676L);
         assertStatsEntry(stats, "rmnet1", 10021, SET_FOREGROUND, 0x30100000, 742L, 3L, 1265L, 3L);
     }
 
     public void testNetworkStatsSingle() throws Exception {
-        stageFile(R.raw.xt_qtaguid_iface_typical, new File(mTestProc, "net/xt_qtaguid/iface_stat_all"));
+        stageFile(R.raw.xt_qtaguid_iface_typical,
+                new File(mTestProc, "net/xt_qtaguid/iface_stat_all"));
 
         final NetworkStats stats = mFactory.readNetworkStatsSummaryDev();
         assertEquals(6, stats.size());
@@ -122,7 +125,8 @@
         final NetworkStats stats = mFactory.readNetworkStatsSummaryXt();
         assertEquals(3, stats.size());
         assertStatsEntry(stats, "rmnet0", UID_ALL, SET_ALL, TAG_NONE, 6824L, 16L, 5692L, 10L);
-        assertStatsEntry(stats, "rmnet1", UID_ALL, SET_ALL, TAG_NONE, 11153922L, 8051L, 190226L, 2468L);
+        assertStatsEntry(stats, "rmnet1", UID_ALL, SET_ALL, TAG_NONE, 11153922L, 8051L, 190226L,
+                2468L);
         assertStatsEntry(stats, "rmnet2", UID_ALL, SET_ALL, TAG_NONE, 4968L, 35L, 3081L, 39L);
     }
 
@@ -157,7 +161,7 @@
 
     private static void assertStatsEntry(NetworkStats stats, String iface, int uid, int set,
             int tag, long rxBytes, long txBytes) {
-        final int i = stats.findIndex(iface, uid, set, tag, ROAMING_NO);
+        final int i = stats.findIndex(iface, uid, set, tag, METERED_NO, ROAMING_NO);
         final NetworkStats.Entry entry = stats.getValues(i, null);
         assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
         assertEquals("unexpected txBytes", txBytes, entry.txBytes);
@@ -165,7 +169,7 @@
 
     private static void assertStatsEntry(NetworkStats stats, String iface, int uid, int set,
             int tag, long rxBytes, long rxPackets, long txBytes, long txPackets) {
-        final int i = stats.findIndex(iface, uid, set, tag, ROAMING_NO);
+        final int i = stats.findIndex(iface, uid, set, tag, METERED_NO, ROAMING_NO);
         final NetworkStats.Entry entry = stats.getValues(i, null);
         assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
         assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets);
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index bdcce22..8c38ed6 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -24,6 +24,7 @@
 import static android.net.ConnectivityManager.getNetworkTypeName;
 import static android.net.ConnectivityManager.isNetworkTypeValid;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -38,7 +39,6 @@
 
 import android.annotation.Nullable;
 import android.app.BroadcastOptions;
-import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
@@ -76,7 +76,9 @@
 import android.net.UidRange;
 import android.net.Uri;
 import android.net.metrics.DefaultNetworkEvent;
+import android.net.metrics.IpConnectivityLog;
 import android.net.metrics.NetworkEvent;
+import android.net.util.AvoidBadWifiTracker;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
@@ -93,6 +95,7 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
+import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -124,14 +127,19 @@
 import com.android.internal.util.AsyncChannel;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.MessageUtils;
+import com.android.internal.util.WakeupMessage;
 import com.android.internal.util.XmlUtils;
 import com.android.server.am.BatteryStatsService;
 import com.android.server.connectivity.DataConnectionStats;
 import com.android.server.connectivity.KeepaliveTracker;
+import com.android.server.connectivity.MockableSystemProperties;
 import com.android.server.connectivity.Nat464Xlat;
+import com.android.server.connectivity.LingerMonitor;
 import com.android.server.connectivity.NetworkAgentInfo;
 import com.android.server.connectivity.NetworkDiagnostics;
 import com.android.server.connectivity.NetworkMonitor;
+import com.android.server.connectivity.NetworkNotificationManager;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
 import com.android.server.connectivity.PacManager;
 import com.android.server.connectivity.PermissionMonitor;
 import com.android.server.connectivity.Tethering;
@@ -170,7 +178,7 @@
  */
 public class ConnectivityService extends IConnectivityManager.Stub
         implements PendingIntent.OnFinished {
-    private static final String TAG = "ConnectivityService";
+    private static final String TAG = ConnectivityService.class.getSimpleName();
 
     private static final boolean DBG = true;
     private static final boolean VDBG = false;
@@ -190,6 +198,12 @@
     // connect anyway?" dialog after the user selects a network that doesn't validate.
     private static final int PROMPT_UNVALIDATED_DELAY_MS = 8 * 1000;
 
+    // Default to 30s linger time-out. Modifiable only for testing.
+    private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger";
+    private static final int DEFAULT_LINGER_DELAY_MS = 30_000;
+    @VisibleForTesting
+    protected int mLingerDelayMs;  // Can't be final, or test subclass constructors can't change it.
+
     // How long to delay to removal of a pending intent based request.
     // See Settings.Secure.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS
     private final int mReleasePendingIntentDelayMs;
@@ -238,7 +252,8 @@
     private static final int DISABLED = 0;
 
     private static final SparseArray<String> sMagicDecoderRing = MessageUtils.findMessageNames(
-            new Class[] { AsyncChannel.class, ConnectivityService.class, NetworkAgent.class });
+            new Class[] { AsyncChannel.class, ConnectivityService.class, NetworkAgent.class,
+                    NetworkAgentInfo.class });
 
     private enum ReapUnvalidatedNetworks {
         // Tear down networks that have no chance (e.g. even if validated) of becoming
@@ -250,6 +265,11 @@
         DONT_REAP
     };
 
+    private enum UnneededFor {
+        LINGER,    // Determine whether this network is unneeded and should be lingered.
+        TEARDOWN,  // Determine whether this network is unneeded and should be torn down.
+    }
+
     /**
      * used internally to change our mobile data enabled flag
      */
@@ -293,7 +313,7 @@
 
     /**
      * indicates a timeout period is over - check if we had a network yet or not
-     * and if not, call the timeout calback (but leave the request live until they
+     * and if not, call the timeout callback (but leave the request live until they
      * cancel it.
      * includes a NetworkRequestInfo
      */
@@ -354,6 +374,11 @@
     private static final int EVENT_SET_ACCEPT_UNVALIDATED = 28;
 
     /**
+     * used to specify whether a network should not be penalized when it becomes unvalidated.
+     */
+    private static final int EVENT_SET_AVOID_UNVALIDATED = 35;
+
+    /**
      * used to ask the user to confirm a connection to an unvalidated network.
      * obj  = network
      */
@@ -370,6 +395,16 @@
      */
     private static final int EVENT_REGISTER_NETWORK_LISTENER_WITH_INTENT = 31;
 
+    /**
+     * Indicates a caller has requested to have its callback invoked with
+     * the latest LinkProperties or NetworkCapabilities.
+     *
+     * arg1 = UID of caller
+     * obj  = NetworkRequest
+     */
+    private static final int EVENT_REQUEST_LINKPROPERTIES  = 32;
+    private static final int EVENT_REQUEST_NETCAPABILITIES = 33;
+
     /** Handler thread used for both of the handlers below. */
     @VisibleForTesting
     protected final HandlerThread mHandlerThread;
@@ -416,6 +451,8 @@
     TelephonyManager mTelephonyManager;
 
     private KeepaliveTracker mKeepaliveTracker;
+    private NetworkNotificationManager mNotifier;
+    private LingerMonitor mLingerMonitor;
 
     // sequence number for Networks; keep in sync with system/netd/NetworkController.cpp
     private final static int MIN_NET_ID = 100; // some reserved marks
@@ -429,6 +466,11 @@
     private static final int MAX_NETWORK_REQUEST_LOGS = 20;
     private final LocalLog mNetworkRequestInfoLogs = new LocalLog(MAX_NETWORK_REQUEST_LOGS);
 
+    // NetworkInfo blocked and unblocked String log entries
+    // TODO: consider reducing memory usage. Each log line is ~40 2B chars, for a total of ~8kB.
+    private static final int MAX_NETWORK_INFO_LOGS = 100;
+    private final LocalLog mNetworkInfoBlockingLogs = new LocalLog(MAX_NETWORK_INFO_LOGS);
+
     // Array of <Network,ReadOnlyLocalLogs> tracking network validation and results
     private static final int MAX_VALIDATION_LOGS = 10;
     private static class ValidationLog {
@@ -454,6 +496,11 @@
         }
     }
 
+    private final IpConnectivityLog mMetricsLog;
+
+    @VisibleForTesting
+    final AvoidBadWifiTracker mAvoidBadWifiTracker;
+
     /**
      * Implements support for the legacy "one network per network type" model.
      *
@@ -650,35 +697,34 @@
 
     public ConnectivityService(Context context, INetworkManagementService netManager,
             INetworkStatsService statsService, INetworkPolicyManager policyManager) {
+        this(context, netManager, statsService, policyManager, new IpConnectivityLog());
+    }
+
+    @VisibleForTesting
+    protected ConnectivityService(Context context, INetworkManagementService netManager,
+            INetworkStatsService statsService, INetworkPolicyManager policyManager,
+            IpConnectivityLog logger) {
         if (DBG) log("ConnectivityService starting up");
 
-        mDefaultRequest = createInternetRequestForTransport(-1);
-        NetworkRequestInfo defaultNRI = new NetworkRequestInfo(null, mDefaultRequest,
-                new Binder(), NetworkRequestType.REQUEST);
+        mMetricsLog = logger;
+        mDefaultRequest = createInternetRequestForTransport(-1, NetworkRequest.Type.REQUEST);
+        NetworkRequestInfo defaultNRI = new NetworkRequestInfo(null, mDefaultRequest, new Binder());
         mNetworkRequests.put(mDefaultRequest, defaultNRI);
         mNetworkRequestInfoLogs.log("REGISTER " + defaultNRI);
 
         mDefaultMobileDataRequest = createInternetRequestForTransport(
-                NetworkCapabilities.TRANSPORT_CELLULAR);
+                NetworkCapabilities.TRANSPORT_CELLULAR, NetworkRequest.Type.BACKGROUND_REQUEST);
 
         mHandlerThread = createHandlerThread();
         mHandlerThread.start();
         mHandler = new InternalHandler(mHandlerThread.getLooper());
         mTrackerHandler = new NetworkStateTrackerHandler(mHandlerThread.getLooper());
 
-        // setup our unique device name
-        if (TextUtils.isEmpty(SystemProperties.get("net.hostname"))) {
-            String id = Settings.Secure.getString(context.getContentResolver(),
-                    Settings.Secure.ANDROID_ID);
-            if (id != null && id.length() > 0) {
-                String name = new String("android-").concat(id);
-                SystemProperties.set("net.hostname", name);
-            }
-        }
-
         mReleasePendingIntentDelayMs = Settings.Secure.getInt(context.getContentResolver(),
                 Settings.Secure.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, 5_000);
 
+        mLingerDelayMs = SystemProperties.getInt(LINGER_DELAY_PROPERTY, DEFAULT_LINGER_DELAY_MS);
+
         mContext = checkNotNull(context, "missing Context");
         mNetd = checkNotNull(netManager, "missing INetworkManagementService");
         mStatsService = checkNotNull(statsService, "missing INetworkStatsService");
@@ -760,7 +806,8 @@
         mTestMode = SystemProperties.get("cm.test.mode").equals("true")
                 && SystemProperties.get("ro.build.type").equals("eng");
 
-        mTethering = new Tethering(mContext, mNetd, statsService);
+        mTethering = new Tethering(mContext, mNetd, statsService, mPolicyManager,
+                                   IoThread.get().getLooper(), new MockableSystemProperties());
 
         mPermissionMonitor = new PermissionMonitor(mContext, mNetd);
 
@@ -796,21 +843,47 @@
         mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
 
         mKeepaliveTracker = new KeepaliveTracker(mHandler);
+        mNotifier = new NetworkNotificationManager(mContext, mTelephonyManager,
+                mContext.getSystemService(NotificationManager.class));
+
+        final int dailyLimit = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT,
+                LingerMonitor.DEFAULT_NOTIFICATION_DAILY_LIMIT);
+        final long rateLimit = Settings.Global.getLong(mContext.getContentResolver(),
+                Settings.Global.NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS,
+                LingerMonitor.DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS);
+        mLingerMonitor = new LingerMonitor(mContext, mNotifier, dailyLimit, rateLimit);
+
+        mAvoidBadWifiTracker = createAvoidBadWifiTracker(
+                mContext, mHandler, () -> rematchForAvoidBadWifiUpdate());
+        mAvoidBadWifiTracker.start();
     }
 
-    private NetworkRequest createInternetRequestForTransport(int transportType) {
+    private NetworkRequest createInternetRequestForTransport(
+            int transportType, NetworkRequest.Type type) {
         NetworkCapabilities netCap = new NetworkCapabilities();
         netCap.addCapability(NET_CAPABILITY_INTERNET);
         netCap.addCapability(NET_CAPABILITY_NOT_RESTRICTED);
         if (transportType > -1) {
             netCap.addTransportType(transportType);
         }
-        return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId());
+        return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId(), type);
+    }
+
+    // Used only for testing.
+    // TODO: Delete this and either:
+    // 1. Give Fake SettingsProvider the ability to send settings change notifications (requires
+    //    changing ContentResolver to make registerContentObserver non-final).
+    // 2. Give FakeSettingsProvider an alternative notification mechanism and have the test use it
+    //    by subclassing SettingsObserver.
+    @VisibleForTesting
+    void updateMobileDataAlwaysOn() {
+        mHandler.sendEmptyMessage(EVENT_CONFIGURE_MOBILE_DATA_ALWAYS_ON);
     }
 
     private void handleMobileDataAlwaysOn() {
         final boolean enable = (Settings.Global.getInt(
-                mContext.getContentResolver(), Settings.Global.MOBILE_DATA_ALWAYS_ON, 0) == 1);
+                mContext.getContentResolver(), Settings.Global.MOBILE_DATA_ALWAYS_ON, 1) == 1);
         final boolean isEnabled = (mNetworkRequests.get(mDefaultMobileDataRequest) != null);
         if (enable == isEnabled) {
             return;  // Nothing to do.
@@ -818,7 +891,7 @@
 
         if (enable) {
             handleRegisterNetworkRequest(new NetworkRequestInfo(
-                    null, mDefaultMobileDataRequest, new Binder(), NetworkRequestType.REQUEST));
+                    null, mDefaultMobileDataRequest, new Binder()));
         } else {
             handleReleaseNetworkRequest(mDefaultMobileDataRequest, Process.SYSTEM_UID);
         }
@@ -978,7 +1051,9 @@
     }
 
     private void maybeLogBlockedNetworkInfo(NetworkInfo ni, int uid) {
-        if (ni == null || !LOGD_BLOCKED_NETWORKINFO) return;
+        if (ni == null || !LOGD_BLOCKED_NETWORKINFO) {
+            return;
+        }
         boolean removed = false;
         boolean added = false;
         synchronized (mBlockedAppUids) {
@@ -988,8 +1063,13 @@
                 removed = true;
             }
         }
-        if (added) log("Returning blocked NetworkInfo to uid=" + uid);
-        else if (removed) log("Returning unblocked NetworkInfo to uid=" + uid);
+        if (added) {
+            log("Returning blocked NetworkInfo to uid=" + uid);
+            mNetworkInfoBlockingLogs.log("BLOCKED " + uid);
+        } else if (removed) {
+            log("Returning unblocked NetworkInfo to uid=" + uid);
+            mNetworkInfoBlockingLogs.log("UNBLOCKED " + uid);
+        }
     }
 
     /**
@@ -1069,6 +1149,7 @@
         return nai != null ? nai.network : null;
     }
 
+    // Public because it's used by mLockdownTracker.
     public NetworkInfo getActiveNetworkInfoUnfiltered() {
         enforceAccessPermission();
         final int uid = Binder.getCallingUid();
@@ -1324,6 +1405,7 @@
      * desired
      * @return {@code true} on success, {@code false} on failure
      */
+    @Override
     public boolean requestRouteToHostAddress(int networkType, byte[] hostAddress) {
         enforceChangePermission();
         if (mProtectedNetworks.contains(networkType)) {
@@ -1528,10 +1610,21 @@
                 "ConnectivityService");
     }
 
+    private void enforceConnectivityRestrictedNetworksPermission() {
+        try {
+            mContext.enforceCallingOrSelfPermission(
+                    android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS,
+                    "ConnectivityService");
+            return;
+        } catch (SecurityException e) { /* fallback to ConnectivityInternalPermission */ }
+        enforceConnectivityInternalPermission();
+    }
+
     private void enforceKeepalivePermission() {
         mContext.enforceCallingOrSelfPermission(KeepaliveTracker.PERMISSION, "ConnectivityService");
     }
 
+    // Public because it's used by mLockdownTracker.
     public void sendConnectedBroadcast(NetworkInfo info) {
         enforceConnectivityInternalPermission();
         sendGeneralBroadcast(info, CONNECTIVITY_ACTION);
@@ -1711,11 +1804,14 @@
     private void updateMtu(LinkProperties newLp, LinkProperties oldLp) {
         final String iface = newLp.getInterfaceName();
         final int mtu = newLp.getMtu();
+        if (oldLp == null && mtu == 0) {
+            // Silently ignore unset MTU value.
+            return;
+        }
         if (oldLp != null && newLp.isIdenticalMtu(oldLp)) {
             if (VDBG) log("identical MTU - not setting");
             return;
         }
-
         if (LinkProperties.isValidMtu(mtu, newLp.hasGlobalIPv6Address()) == false) {
             if (mtu != 0) loge("Unexpected mtu value: " + mtu + ", " + iface);
             return;
@@ -1888,15 +1984,20 @@
         for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
             pw.println(nai.toString());
             pw.increaseIndent();
-            pw.println("Requests:");
+            pw.println(String.format(
+                    "Requests: REQUEST:%d LISTEN:%d BACKGROUND_REQUEST:%d total:%d",
+                    nai.numForegroundNetworkRequests(),
+                    nai.numNetworkRequests() - nai.numRequestNetworkRequests(),
+                    nai.numBackgroundNetworkRequests(),
+                    nai.numNetworkRequests()));
             pw.increaseIndent();
-            for (int i = 0; i < nai.networkRequests.size(); i++) {
-                pw.println(nai.networkRequests.valueAt(i).toString());
+            for (int i = 0; i < nai.numNetworkRequests(); i++) {
+                pw.println(nai.requestAt(i).toString());
             }
             pw.decreaseIndent();
             pw.println("Lingered:");
             pw.increaseIndent();
-            for (NetworkRequest nr : nai.networkLingered) pw.println(nr.toString());
+            nai.dumpLingerTimers(pw);
             pw.decreaseIndent();
             pw.decreaseIndent();
         }
@@ -1957,7 +2058,9 @@
         mKeepaliveTracker.dump(pw);
 
         pw.println();
+        dumpAvoidBadWifiSettings(pw);
 
+        pw.println();
         if (mInetLog != null && mInetLog.size() > 0) {
             pw.println();
             pw.println("Inet condition reports:");
@@ -1985,6 +2088,12 @@
             pw.increaseIndent();
             mNetworkRequestInfoLogs.reverseDump(fd, pw, args);
             pw.decreaseIndent();
+
+            pw.println();
+            pw.println("mNetworkInfoBlockingLogs (most recent first):");
+            pw.increaseIndent();
+            mNetworkInfoBlockingLogs.reverseDump(fd, pw, args);
+            pw.decreaseIndent();
         }
     }
 
@@ -2000,10 +2109,6 @@
         return false;
     }
 
-    private boolean isRequest(NetworkRequest request) {
-        return mNetworkRequests.get(request).isRequest();
-    }
-
     // must be stateless - things change under us.
     private class NetworkStateTrackerHandler extends Handler {
         public NetworkStateTrackerHandler(Looper looper) {
@@ -2045,15 +2150,11 @@
                 case NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED: {
                     final NetworkCapabilities networkCapabilities = (NetworkCapabilities) msg.obj;
                     if (networkCapabilities.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) ||
-                            networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED)) {
+                            networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) ||
+                            networkCapabilities.hasCapability(NET_CAPABILITY_FOREGROUND)) {
                         Slog.wtf(TAG, "BUG: " + nai + " has CS-managed capability.");
                     }
-                    if (nai.everConnected && !nai.networkCapabilities.equalImmutableCapabilities(
-                            networkCapabilities)) {
-                        Slog.wtf(TAG, "BUG: " + nai + " changed immutable capabilities: "
-                                + nai.networkCapabilities + " -> " + networkCapabilities);
-                    }
-                    updateCapabilities(nai, networkCapabilities);
+                    updateCapabilities(nai.getCurrentScore(), nai, networkCapabilities);
                     break;
                 }
                 case NetworkAgent.EVENT_NETWORK_PROPERTIES_CHANGED: {
@@ -2124,13 +2225,14 @@
                     if (nai != null) {
                         final boolean valid =
                                 (msg.arg1 == NetworkMonitor.NETWORK_TEST_RESULT_VALID);
+                        final boolean wasValidated = nai.lastValidated;
                         if (DBG) log(nai.name() + " validation " + (valid ? "passed" : "failed") +
                                 (msg.obj == null ? "" : " with redirect to " + (String)msg.obj));
                         if (valid != nai.lastValidated) {
                             final int oldScore = nai.getCurrentScore();
                             nai.lastValidated = valid;
                             nai.everValidated |= valid;
-                            updateCapabilities(nai, nai.networkCapabilities);
+                            updateCapabilities(oldScore, nai, nai.networkCapabilities);
                             // If score has changed, rebroadcast to NetworkFactories. b/17726566
                             if (oldScore != nai.getCurrentScore()) sendUpdatedScoreToFactories(nai);
                         }
@@ -2142,13 +2244,9 @@
                                 NetworkAgent.CMD_REPORT_NETWORK_STATUS,
                                 (valid ? NetworkAgent.VALID_NETWORK : NetworkAgent.INVALID_NETWORK),
                                 0, redirectUrlBundle);
-                    }
-                    break;
-                }
-                case NetworkMonitor.EVENT_NETWORK_LINGER_COMPLETE: {
-                    NetworkAgentInfo nai = (NetworkAgentInfo)msg.obj;
-                    if (isLiveNetworkAgent(nai, msg.what)) {
-                        handleLingerComplete(nai);
+                         if (wasValidated && !nai.lastValidated) {
+                             handleNetworkUnvalidated(nai);
+                         }
                     }
                     break;
                 }
@@ -2159,22 +2257,53 @@
                     synchronized (mNetworkForNetId) {
                         nai = mNetworkForNetId.get(netId);
                     }
-                    // If captive portal status has changed, update capabilities.
+                    // If captive portal status has changed, update capabilities or disconnect.
                     if (nai != null && (visible != nai.lastCaptivePortalDetected)) {
+                        final int oldScore = nai.getCurrentScore();
                         nai.lastCaptivePortalDetected = visible;
                         nai.everCaptivePortalDetected |= visible;
-                        updateCapabilities(nai, nai.networkCapabilities);
+                        if (nai.lastCaptivePortalDetected &&
+                            Settings.Global.CAPTIVE_PORTAL_MODE_AVOID == getCaptivePortalMode()) {
+                            if (DBG) log("Avoiding captive portal network: " + nai.name());
+                            nai.asyncChannel.sendMessage(
+                                    NetworkAgent.CMD_PREVENT_AUTOMATIC_RECONNECT);
+                            teardownUnneededNetwork(nai);
+                            break;
+                        }
+                        updateCapabilities(oldScore, nai, nai.networkCapabilities);
                     }
                     if (!visible) {
-                        setProvNotificationVisibleIntent(false, netId, null, 0, null, null, false);
+                        mNotifier.clearNotification(netId);
                     } else {
                         if (nai == null) {
                             loge("EVENT_PROVISIONING_NOTIFICATION from unknown NetworkMonitor");
                             break;
                         }
-                        setProvNotificationVisibleIntent(true, netId, NotificationType.SIGN_IN,
-                                nai.networkInfo.getType(), nai.networkInfo.getExtraInfo(),
-                                (PendingIntent)msg.obj, nai.networkMisc.explicitlySelected);
+                        if (!nai.networkMisc.provisioningNotificationDisabled) {
+                            mNotifier.showNotification(netId, NotificationType.SIGN_IN, nai, null,
+                                    (PendingIntent) msg.obj, nai.networkMisc.explicitlySelected);
+                        }
+                    }
+                    break;
+                }
+            }
+            return true;
+        }
+
+        private int getCaptivePortalMode() {
+            return Settings.Global.getInt(mContext.getContentResolver(),
+                    Settings.Global.CAPTIVE_PORTAL_MODE,
+                    Settings.Global.CAPTIVE_PORTAL_MODE_PROMPT);
+        }
+
+        private boolean maybeHandleNetworkAgentInfoMessage(Message msg) {
+            switch (msg.what) {
+                default:
+                    return false;
+                case NetworkAgentInfo.EVENT_NETWORK_LINGER_COMPLETE: {
+                    NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj;
+                    if (nai != null && isLiveNetworkAgent(nai, msg.what)) {
+                        handleLingerComplete(nai);
                     }
                     break;
                 }
@@ -2184,31 +2313,31 @@
 
         @Override
         public void handleMessage(Message msg) {
-            if (!maybeHandleAsyncChannelMessage(msg) && !maybeHandleNetworkMonitorMessage(msg)) {
+            if (!maybeHandleAsyncChannelMessage(msg) &&
+                    !maybeHandleNetworkMonitorMessage(msg) &&
+                    !maybeHandleNetworkAgentInfoMessage(msg)) {
                 maybeHandleNetworkAgentMessage(msg);
             }
         }
     }
 
-    private void linger(NetworkAgentInfo nai) {
-        nai.lingering = true;
-        NetworkEvent.logEvent(nai.network.netId, NetworkEvent.NETWORK_LINGER);
-        nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_LINGER);
-        notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING);
-    }
-
-    // Cancel any lingering so the linger timeout doesn't teardown a network.
-    // This should be called when a network begins satisfying a NetworkRequest.
-    // Note: depending on what state the NetworkMonitor is in (e.g.,
-    // if it's awaiting captive portal login, or if validation failed), this
-    // may trigger a re-evaluation of the network.
-    private void unlinger(NetworkAgentInfo nai) {
-        nai.networkLingered.clear();
-        if (!nai.lingering) return;
-        nai.lingering = false;
-        NetworkEvent.logEvent(nai.network.netId, NetworkEvent.NETWORK_UNLINGER);
-        if (VDBG) log("Canceling linger of " + nai.name());
-        nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_CONNECTED);
+    private void updateLingerState(NetworkAgentInfo nai, long now) {
+        // 1. Update the linger timer. If it's changed, reschedule or cancel the alarm.
+        // 2. If the network was lingering and there are now requests, unlinger it.
+        // 3. If this network is unneeded (which implies it is not lingering), and there is at least
+        //    one lingered request, start lingering.
+        nai.updateLingerTimer();
+        if (nai.isLingering() && nai.numForegroundNetworkRequests() > 0) {
+            if (DBG) log("Unlingering " + nai.name());
+            nai.unlinger();
+            logNetworkEvent(nai, NetworkEvent.NETWORK_UNLINGER);
+        } else if (unneeded(nai, UnneededFor.LINGER) && nai.getLingerExpiry() > 0) {
+            int lingerTime = (int) (nai.getLingerExpiry() - now);
+            if (DBG) log("Lingering " + nai.name() + " for " + lingerTime + "ms");
+            nai.linger();
+            logNetworkEvent(nai, NetworkEvent.NETWORK_LINGER);
+            notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING, lingerTime);
+        }
     }
 
     private void handleAsyncChannelHalfConnect(Message msg) {
@@ -2218,7 +2347,7 @@
                 if (VDBG) log("NetworkFactory connected");
                 // A network factory has connected.  Send it all current NetworkRequests.
                 for (NetworkRequestInfo nri : mNetworkRequests.values()) {
-                    if (!nri.isRequest()) continue;
+                    if (nri.request.isListen()) continue;
                     NetworkAgentInfo nai = mNetworkForRequestId.get(nri.request.requestId);
                     ac.sendMessage(android.net.NetworkFactory.CMD_REQUEST_NETWORK,
                             (nai != null ? nai.getCurrentScore() : 0), 0, nri.request);
@@ -2253,7 +2382,7 @@
         NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo);
         if (nai != null) {
             if (DBG) {
-                log(nai.name() + " got DISCONNECTED, was satisfying " + nai.networkRequests.size());
+                log(nai.name() + " got DISCONNECTED, was satisfying " + nai.numNetworkRequests());
             }
             // A network agent has disconnected.
             // TODO - if we move the logic to the network agent (have them disconnect
@@ -2290,21 +2419,23 @@
                 mNetworkForNetId.remove(nai.network.netId);
             }
             // Remove all previously satisfied requests.
-            for (int i = 0; i < nai.networkRequests.size(); i++) {
-                NetworkRequest request = nai.networkRequests.valueAt(i);
+            for (int i = 0; i < nai.numNetworkRequests(); i++) {
+                NetworkRequest request = nai.requestAt(i);
                 NetworkAgentInfo currentNetwork = mNetworkForRequestId.get(request.requestId);
                 if (currentNetwork != null && currentNetwork.network.netId == nai.network.netId) {
                     mNetworkForRequestId.remove(request.requestId);
                     sendUpdatedScoreToFactories(request, 0);
                 }
             }
-            if (nai.networkRequests.get(mDefaultRequest.requestId) != null) {
+            nai.clearLingerState();
+            if (nai.isSatisfyingRequest(mDefaultRequest.requestId)) {
                 removeDataActivityTracking(nai);
                 notifyLockdownVpn(nai);
                 requestNetworkTransitionWakelock(nai.name());
             }
             mLegacyTypeTracker.remove(nai, wasDefault);
             rematchAllNetworksAndRequests(null, 0);
+            mLingerMonitor.noteDisconnect(nai);
             if (nai.created) {
                 // Tell netd to clean up the configuration for this network
                 // (routing rules, DNS, etc).
@@ -2359,7 +2490,7 @@
     private void handleRegisterNetworkRequest(NetworkRequestInfo nri) {
         mNetworkRequests.put(nri.request, nri);
         mNetworkRequestInfoLogs.log("REGISTER " + nri);
-        if (!nri.isRequest()) {
+        if (nri.request.isListen()) {
             for (NetworkAgentInfo network : mNetworkAgentInfos.values()) {
                 if (nri.request.networkCapabilities.hasSignalStrength() &&
                         network.satisfiesImmutableCapabilitiesOf(nri.request)) {
@@ -2368,7 +2499,7 @@
             }
         }
         rematchAllNetworksAndRequests(null, 0);
-        if (nri.isRequest() && mNetworkForRequestId.get(nri.request.requestId) == null) {
+        if (nri.request.isRequest() && mNetworkForRequestId.get(nri.request.requestId) == null) {
             sendUpdatedScoreToFactories(nri.request, 0);
         }
     }
@@ -2381,16 +2512,41 @@
         }
     }
 
-    // Is nai unneeded by all NetworkRequests (and should be disconnected)?
-    // This is whether it is satisfying any NetworkRequests or were it to become validated,
-    // would it have a chance of satisfying any NetworkRequests.
-    private boolean unneeded(NetworkAgentInfo nai) {
-        if (!nai.everConnected || nai.isVPN() || nai.lingering) return false;
+    // Determines whether the network is the best (or could become the best, if it validated), for
+    // none of a particular type of NetworkRequests. The type of NetworkRequests considered depends
+    // on the value of reason:
+    //
+    // - UnneededFor.TEARDOWN: non-listen NetworkRequests. If a network is unneeded for this reason,
+    //   then it should be torn down.
+    // - UnneededFor.LINGER: foreground NetworkRequests. If a network is unneeded for this reason,
+    //   then it should be lingered.
+    private boolean unneeded(NetworkAgentInfo nai, UnneededFor reason) {
+        final int numRequests;
+        switch (reason) {
+            case TEARDOWN:
+                numRequests = nai.numRequestNetworkRequests();
+                break;
+            case LINGER:
+                numRequests = nai.numForegroundNetworkRequests();
+                break;
+            default:
+                Slog.wtf(TAG, "Invalid reason. Cannot happen.");
+                return true;
+        }
+
+        if (!nai.everConnected || nai.isVPN() || nai.isLingering() || numRequests > 0) {
+            return false;
+        }
         for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+            if (reason == UnneededFor.LINGER && nri.request.isBackgroundRequest()) {
+                // Background requests don't affect lingering.
+                continue;
+            }
+
             // If this Network is already the highest scoring Network for a request, or if
             // there is hope for it to become one if it validated, then it is needed.
-            if (nri.isRequest() && nai.satisfies(nri.request) &&
-                    (nai.networkRequests.get(nri.request.requestId) != null ||
+            if (nri.request.isRequest() && nai.satisfies(nri.request) &&
+                    (nai.isSatisfyingRequest(nri.request.requestId) ||
                     // Note that this catches two important cases:
                     // 1. Unvalidated cellular will not be reaped when unvalidated WiFi
                     //    is currently satisfying the request.  This is desirable when
@@ -2406,108 +2562,180 @@
         return true;
     }
 
-    private void handleReleaseNetworkRequest(NetworkRequest request, int callingUid) {
-        NetworkRequestInfo nri = mNetworkRequests.get(request);
+    private NetworkRequestInfo getNriForAppRequest(
+            NetworkRequest request, int callingUid, String requestedOperation) {
+        final NetworkRequestInfo nri = mNetworkRequests.get(request);
+
         if (nri != null) {
             if (Process.SYSTEM_UID != callingUid && nri.mUid != callingUid) {
-                if (DBG) log("Attempt to release unowned NetworkRequest " + request);
-                return;
+                log(String.format("UID %d attempted to %s for unowned request %s",
+                        callingUid, requestedOperation, nri));
+                return null;
             }
-            if (VDBG || (DBG && nri.isRequest())) log("releasing NetworkRequest " + request);
-            nri.unlinkDeathRecipient();
-            mNetworkRequests.remove(request);
-            synchronized (mUidToNetworkRequestCount) {
-                int requests = mUidToNetworkRequestCount.get(nri.mUid, 0);
-                if (requests < 1) {
-                    Slog.wtf(TAG, "BUG: too small request count " + requests + " for UID " +
-                            nri.mUid);
-                } else if (requests == 1) {
-                    mUidToNetworkRequestCount.removeAt(
-                            mUidToNetworkRequestCount.indexOfKey(nri.mUid));
-                } else {
-                    mUidToNetworkRequestCount.put(nri.mUid, requests - 1);
-                }
-            }
-            mNetworkRequestInfoLogs.log("RELEASE " + nri);
-            if (nri.isRequest()) {
-                // Find all networks that are satisfying this request and remove the request
-                // from their request lists.
-                // TODO - it's my understanding that for a request there is only a single
-                // network satisfying it, so this loop is wasteful
-                boolean wasKept = false;
-                for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
-                    if (nai.networkRequests.get(nri.request.requestId) != null) {
-                        nai.networkRequests.remove(nri.request.requestId);
-                        if (VDBG) {
-                            log(" Removing from current network " + nai.name() +
-                                    ", leaving " + nai.networkRequests.size() +
-                                    " requests.");
-                        }
-                        if (unneeded(nai)) {
-                            if (DBG) log("no live requests for " + nai.name() + "; disconnecting");
-                            teardownUnneededNetwork(nai);
-                        } else {
-                            // suspect there should only be one pass through here
-                            // but if any were kept do the check below
-                            wasKept |= true;
-                        }
-                    }
-                }
+        }
 
-                NetworkAgentInfo nai = mNetworkForRequestId.get(nri.request.requestId);
-                if (nai != null) {
-                    mNetworkForRequestId.remove(nri.request.requestId);
-                }
-                // Maintain the illusion.  When this request arrived, we might have pretended
-                // that a network connected to serve it, even though the network was already
-                // connected.  Now that this request has gone away, we might have to pretend
-                // that the network disconnected.  LegacyTypeTracker will generate that
-                // phantom disconnect for this type.
-                if (nri.request.legacyType != TYPE_NONE && nai != null) {
-                    boolean doRemove = true;
-                    if (wasKept) {
-                        // check if any of the remaining requests for this network are for the
-                        // same legacy type - if so, don't remove the nai
-                        for (int i = 0; i < nai.networkRequests.size(); i++) {
-                            NetworkRequest otherRequest = nai.networkRequests.valueAt(i);
-                            if (otherRequest.legacyType == nri.request.legacyType &&
-                                    isRequest(otherRequest)) {
-                                if (DBG) log(" still have other legacy request - leaving");
-                                doRemove = false;
-                            }
-                        }
-                    }
+        return nri;
+    }
 
-                    if (doRemove) {
-                        mLegacyTypeTracker.remove(nri.request.legacyType, nai, false);
-                    }
-                }
+    private void handleRequestCallbackUpdate(NetworkRequest request, int callingUid,
+            String description, int callbackType) {
+        final NetworkRequestInfo nri = getNriForAppRequest(request, callingUid, description);
+        if (nri == null) return;
 
-                for (NetworkFactoryInfo nfi : mNetworkFactoryInfos.values()) {
-                    nfi.asyncChannel.sendMessage(android.net.NetworkFactory.CMD_CANCEL_REQUEST,
-                            nri.request);
-                }
-            } else {
-                // listens don't have a singular affectedNetwork.  Check all networks to see
-                // if this listen request applies and remove it.
-                for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
-                    nai.networkRequests.remove(nri.request.requestId);
-                    if (nri.request.networkCapabilities.hasSignalStrength() &&
-                            nai.satisfiesImmutableCapabilitiesOf(nri.request)) {
-                        updateSignalStrengthThresholds(nai, "RELEASE", nri.request);
-                    }
-                }
-            }
-            callCallbackForRequest(nri, null, ConnectivityManager.CALLBACK_RELEASED);
+        final NetworkAgentInfo nai = mNetworkForRequestId.get(nri.request.requestId);
+        // The network that is satisfying this request may have changed since
+        // the application requested the update.
+        //
+        // - If the request is no longer satisfied, don't send any updates.
+        // - If the request is satisfied by a different network, it is the
+        //   caller's responsibility to check that the Network object in the
+        //   callback matches the network that was returned in the last
+        //   onAvailable() callback for this request.
+        if (nai == null) return;
+        callCallbackForRequest(nri, nai, callbackType, 0);
+    }
+
+    private void handleRequestLinkProperties(NetworkRequest request, int callingUid) {
+        handleRequestCallbackUpdate(request, callingUid,
+                "request LinkProperties", ConnectivityManager.CALLBACK_IP_CHANGED);
+    }
+
+    private void handleRequestNetworkCapabilities(NetworkRequest request, int callingUid) {
+        handleRequestCallbackUpdate(request, callingUid,
+                "request NetworkCapabilities", ConnectivityManager.CALLBACK_CAP_CHANGED);
+    }
+
+    private void handleTimedOutNetworkRequest(final NetworkRequestInfo nri) {
+        if (mNetworkRequests.get(nri.request) != null && mNetworkForRequestId.get(
+                nri.request.requestId) == null) {
+            handleRemoveNetworkRequest(nri, ConnectivityManager.CALLBACK_UNAVAIL);
         }
     }
 
+    private void handleReleaseNetworkRequest(NetworkRequest request, int callingUid) {
+        final NetworkRequestInfo nri = getNriForAppRequest(
+                request, callingUid, "release NetworkRequest");
+        if (nri != null) {
+            handleRemoveNetworkRequest(nri, ConnectivityManager.CALLBACK_RELEASED);
+        }
+    }
+
+    private void handleRemoveNetworkRequest(final NetworkRequestInfo nri, final int whichCallback) {
+        final String logCallbackType = ConnectivityManager.getCallbackName(whichCallback);
+        if (VDBG || (DBG && nri.request.isRequest())) {
+            log("releasing " + nri.request + " (" + logCallbackType + ")");
+        }
+        nri.unlinkDeathRecipient();
+        mNetworkRequests.remove(nri.request);
+        synchronized (mUidToNetworkRequestCount) {
+            int requests = mUidToNetworkRequestCount.get(nri.mUid, 0);
+            if (requests < 1) {
+                Slog.wtf(TAG, "BUG: too small request count " + requests + " for UID " +
+                        nri.mUid);
+            } else if (requests == 1) {
+                mUidToNetworkRequestCount.removeAt(
+                        mUidToNetworkRequestCount.indexOfKey(nri.mUid));
+            } else {
+                mUidToNetworkRequestCount.put(nri.mUid, requests - 1);
+            }
+        }
+        mNetworkRequestInfoLogs.log("RELEASE " + nri);
+        if (nri.request.isRequest()) {
+            boolean wasKept = false;
+            NetworkAgentInfo nai = mNetworkForRequestId.get(nri.request.requestId);
+            if (nai != null) {
+                boolean wasBackgroundNetwork = nai.isBackgroundNetwork();
+                nai.removeRequest(nri.request.requestId);
+                if (VDBG) {
+                    log(" Removing from current network " + nai.name() +
+                            ", leaving " + nai.numNetworkRequests() + " requests.");
+                }
+                // If there are still lingered requests on this network, don't tear it down,
+                // but resume lingering instead.
+                updateLingerState(nai, SystemClock.elapsedRealtime());
+                if (unneeded(nai, UnneededFor.TEARDOWN)) {
+                    if (DBG) log("no live requests for " + nai.name() + "; disconnecting");
+                    teardownUnneededNetwork(nai);
+                } else {
+                    wasKept = true;
+                }
+                mNetworkForRequestId.remove(nri.request.requestId);
+                if (!wasBackgroundNetwork && nai.isBackgroundNetwork()) {
+                    // Went from foreground to background.
+                    updateCapabilities(nai.getCurrentScore(), nai, nai.networkCapabilities);
+                }
+            }
+
+            // TODO: remove this code once we know that the Slog.wtf is never hit.
+            //
+            // Find all networks that are satisfying this request and remove the request
+            // from their request lists.
+            // TODO - it's my understanding that for a request there is only a single
+            // network satisfying it, so this loop is wasteful
+            for (NetworkAgentInfo otherNai : mNetworkAgentInfos.values()) {
+                if (otherNai.isSatisfyingRequest(nri.request.requestId) && otherNai != nai) {
+                    Slog.wtf(TAG, "Request " + nri.request + " satisfied by " +
+                            otherNai.name() + ", but mNetworkAgentInfos says " +
+                            (nai != null ? nai.name() : "null"));
+                }
+            }
+
+            // Maintain the illusion.  When this request arrived, we might have pretended
+            // that a network connected to serve it, even though the network was already
+            // connected.  Now that this request has gone away, we might have to pretend
+            // that the network disconnected.  LegacyTypeTracker will generate that
+            // phantom disconnect for this type.
+            if (nri.request.legacyType != TYPE_NONE && nai != null) {
+                boolean doRemove = true;
+                if (wasKept) {
+                    // check if any of the remaining requests for this network are for the
+                    // same legacy type - if so, don't remove the nai
+                    for (int i = 0; i < nai.numNetworkRequests(); i++) {
+                        NetworkRequest otherRequest = nai.requestAt(i);
+                        if (otherRequest.legacyType == nri.request.legacyType &&
+                                otherRequest.isRequest()) {
+                            if (DBG) log(" still have other legacy request - leaving");
+                            doRemove = false;
+                        }
+                    }
+                }
+
+                if (doRemove) {
+                    mLegacyTypeTracker.remove(nri.request.legacyType, nai, false);
+                }
+            }
+
+            for (NetworkFactoryInfo nfi : mNetworkFactoryInfos.values()) {
+                nfi.asyncChannel.sendMessage(android.net.NetworkFactory.CMD_CANCEL_REQUEST,
+                        nri.request);
+            }
+        } else {
+            // listens don't have a singular affectedNetwork.  Check all networks to see
+            // if this listen request applies and remove it.
+            for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
+                nai.removeRequest(nri.request.requestId);
+                if (nri.request.networkCapabilities.hasSignalStrength() &&
+                        nai.satisfiesImmutableCapabilitiesOf(nri.request)) {
+                    updateSignalStrengthThresholds(nai, "RELEASE", nri.request);
+                }
+            }
+        }
+        callCallbackForRequest(nri, null, whichCallback, 0);
+    }
+
+    @Override
     public void setAcceptUnvalidated(Network network, boolean accept, boolean always) {
         enforceConnectivityInternalPermission();
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_ACCEPT_UNVALIDATED,
                 accept ? 1 : 0, always ? 1: 0, network));
     }
 
+    @Override
+    public void setAvoidUnvalidated(Network network) {
+        enforceConnectivityInternalPermission();
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_AVOID_UNVALIDATED, network));
+    }
+
     private void handleSetAcceptUnvalidated(Network network, boolean accept, boolean always) {
         if (DBG) log("handleSetAcceptUnvalidated network=" + network +
                 " accept=" + accept + " always=" + always);
@@ -2548,6 +2776,20 @@
 
     }
 
+    private void handleSetAvoidUnvalidated(Network network) {
+        NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+        if (nai == null || nai.lastValidated) {
+            // Nothing to do. The network either disconnected or revalidated.
+            return;
+        }
+        if (!nai.avoidUnvalidated) {
+            int oldScore = nai.getCurrentScore();
+            nai.avoidUnvalidated = true;
+            rematchAllNetworksAndRequests(nai, oldScore);
+            sendUpdatedScoreToFactories(nai);
+        }
+    }
+
     private void scheduleUnvalidatedPrompt(NetworkAgentInfo nai) {
         if (VDBG) log("scheduleUnvalidatedPrompt " + nai.network);
         mHandler.sendMessageDelayed(
@@ -2555,6 +2797,81 @@
                 PROMPT_UNVALIDATED_DELAY_MS);
     }
 
+    public boolean avoidBadWifi() {
+        return mAvoidBadWifiTracker.currentValue();
+    }
+
+    private void rematchForAvoidBadWifiUpdate() {
+        rematchAllNetworksAndRequests(null, 0);
+        for (NetworkAgentInfo nai: mNetworkAgentInfos.values()) {
+            if (nai.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+                sendUpdatedScoreToFactories(nai);
+            }
+        }
+    }
+
+    // TODO: Evaluate whether this is of interest to other consumers of
+    // AvoidBadWifiTracker and worth moving out of here.
+    private void dumpAvoidBadWifiSettings(IndentingPrintWriter pw) {
+        final boolean configRestrict = mAvoidBadWifiTracker.configRestrictsAvoidBadWifi();
+        if (!configRestrict) {
+            pw.println("Bad Wi-Fi avoidance: unrestricted");
+            return;
+        }
+
+        pw.println("Bad Wi-Fi avoidance: " + avoidBadWifi());
+        pw.increaseIndent();
+        pw.println("Config restrict:   " + configRestrict);
+
+        final String value = mAvoidBadWifiTracker.getSettingsValue();
+        String description;
+        // Can't use a switch statement because strings are legal case labels, but null is not.
+        if ("0".equals(value)) {
+            description = "get stuck";
+        } else if (value == null) {
+            description = "prompt";
+        } else if ("1".equals(value)) {
+            description = "avoid";
+        } else {
+            description = value + " (?)";
+        }
+        pw.println("User setting:      " + description);
+        pw.println("Network overrides:");
+        pw.increaseIndent();
+        for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
+            if (nai.avoidUnvalidated) {
+                pw.println(nai.name());
+            }
+        }
+        pw.decreaseIndent();
+        pw.decreaseIndent();
+    }
+
+    private void showValidationNotification(NetworkAgentInfo nai, NotificationType type) {
+        final String action;
+        switch (type) {
+            case NO_INTERNET:
+                action = ConnectivityManager.ACTION_PROMPT_UNVALIDATED;
+                break;
+            case LOST_INTERNET:
+                action = ConnectivityManager.ACTION_PROMPT_LOST_VALIDATION;
+                break;
+            default:
+                Slog.wtf(TAG, "Unknown notification type " + type);
+                return;
+        }
+
+        Intent intent = new Intent(action);
+        intent.setData(Uri.fromParts("netId", Integer.toString(nai.network.netId), null));
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.setClassName("com.android.settings",
+                "com.android.settings.wifi.WifiNoInternetDialog");
+
+        PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
+                mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
+        mNotifier.showNotification(nai.network.netId, type, nai, null, pendingIntent, true);
+    }
+
     private void handlePromptUnvalidated(Network network) {
         if (VDBG) log("handlePromptUnvalidated " + network);
         NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
@@ -2566,17 +2883,17 @@
                 !nai.networkMisc.explicitlySelected || nai.networkMisc.acceptUnvalidated) {
             return;
         }
+        showValidationNotification(nai, NotificationType.NO_INTERNET);
+    }
 
-        Intent intent = new Intent(ConnectivityManager.ACTION_PROMPT_UNVALIDATED);
-        intent.setData(Uri.fromParts("netId", Integer.toString(network.netId), null));
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        intent.setClassName("com.android.settings",
-                "com.android.settings.wifi.WifiNoInternetDialog");
+    private void handleNetworkUnvalidated(NetworkAgentInfo nai) {
+        NetworkCapabilities nc = nai.networkCapabilities;
+        if (DBG) log("handleNetworkUnvalidated " + nai.name() + " cap=" + nc);
 
-        PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
-                mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
-        setProvNotificationVisibleIntent(true, nai.network.netId, NotificationType.NO_INTERNET,
-                nai.networkInfo.getType(), nai.networkInfo.getExtraInfo(), pendingIntent, true);
+        if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
+            mAvoidBadWifiTracker.shouldNotifyWifiUnvalidated()) {
+            showValidationNotification(nai, NotificationType.LOST_INTERNET);
+        }
     }
 
     private class InternalHandler extends Handler {
@@ -2640,6 +2957,11 @@
                     handleRegisterNetworkRequestWithIntent(msg);
                     break;
                 }
+                case EVENT_TIMEOUT_NETWORK_REQUEST: {
+                    NetworkRequestInfo nri = (NetworkRequestInfo) msg.obj;
+                    handleTimedOutNetworkRequest(nri);
+                    break;
+                }
                 case EVENT_RELEASE_NETWORK_REQUEST_WITH_INTENT: {
                     handleReleaseNetworkRequestWithIntent((PendingIntent) msg.obj, msg.arg1);
                     break;
@@ -2652,6 +2974,10 @@
                     handleSetAcceptUnvalidated((Network) msg.obj, msg.arg1 != 0, msg.arg2 != 0);
                     break;
                 }
+                case EVENT_SET_AVOID_UNVALIDATED: {
+                    handleSetAvoidUnvalidated((Network) msg.obj);
+                    break;
+                }
                 case EVENT_PROMPT_UNVALIDATED: {
                     handlePromptUnvalidated((Network) msg.obj);
                     break;
@@ -2660,6 +2986,12 @@
                     handleMobileDataAlwaysOn();
                     break;
                 }
+                case EVENT_REQUEST_LINKPROPERTIES:
+                    handleRequestLinkProperties((NetworkRequest) msg.obj, msg.arg1);
+                    break;
+                case EVENT_REQUEST_NETCAPABILITIES:
+                    handleRequestNetworkCapabilities((NetworkRequest) msg.obj, msg.arg1);
+                    break;
                 // Sent by KeepaliveTracker to process an app request on the state machine thread.
                 case NetworkAgent.CMD_START_PACKET_KEEPALIVE: {
                     mKeepaliveTracker.handleStartKeepalive(msg);
@@ -2684,16 +3016,11 @@
     }
 
     // javadoc from interface
+    @Override
     public int tether(String iface) {
         ConnectivityManager.enforceTetherChangePermission(mContext);
         if (isTetheringSupported()) {
             final int status = mTethering.tether(iface);
-            if (status == ConnectivityManager.TETHER_ERROR_NO_ERROR) {
-                try {
-                    mPolicyManager.onTetheringChanged(iface, true);
-                } catch (RemoteException e) {
-                }
-            }
             return status;
         } else {
             return ConnectivityManager.TETHER_ERROR_UNSUPPORTED;
@@ -2701,17 +3028,12 @@
     }
 
     // javadoc from interface
+    @Override
     public int untether(String iface) {
         ConnectivityManager.enforceTetherChangePermission(mContext);
 
         if (isTetheringSupported()) {
             final int status = mTethering.untether(iface);
-            if (status == ConnectivityManager.TETHER_ERROR_NO_ERROR) {
-                try {
-                    mPolicyManager.onTetheringChanged(iface, false);
-                } catch (RemoteException e) {
-                }
-            }
             return status;
         } else {
             return ConnectivityManager.TETHER_ERROR_UNSUPPORTED;
@@ -2719,6 +3041,7 @@
     }
 
     // javadoc from interface
+    @Override
     public int getLastTetherError(String iface) {
         enforceTetherAccessPermission();
 
@@ -2730,6 +3053,7 @@
     }
 
     // TODO - proper iface API for selection by property, inspection, etc
+    @Override
     public String[] getTetherableUsbRegexs() {
         enforceTetherAccessPermission();
         if (isTetheringSupported()) {
@@ -2739,6 +3063,7 @@
         }
     }
 
+    @Override
     public String[] getTetherableWifiRegexs() {
         enforceTetherAccessPermission();
         if (isTetheringSupported()) {
@@ -2748,6 +3073,7 @@
         }
     }
 
+    @Override
     public String[] getTetherableBluetoothRegexs() {
         enforceTetherAccessPermission();
         if (isTetheringSupported()) {
@@ -2757,6 +3083,7 @@
         }
     }
 
+    @Override
     public int setUsbTethering(boolean enable) {
         ConnectivityManager.enforceTetherChangePermission(mContext);
         if (isTetheringSupported()) {
@@ -2768,21 +3095,25 @@
 
     // TODO - move iface listing, queries, etc to new module
     // javadoc from interface
+    @Override
     public String[] getTetherableIfaces() {
         enforceTetherAccessPermission();
         return mTethering.getTetherableIfaces();
     }
 
+    @Override
     public String[] getTetheredIfaces() {
         enforceTetherAccessPermission();
         return mTethering.getTetheredIfaces();
     }
 
+    @Override
     public String[] getTetheringErroredIfaces() {
         enforceTetherAccessPermission();
         return mTethering.getErroredIfaces();
     }
 
+    @Override
     public String[] getTetheredDhcpRanges() {
         enforceConnectivityInternalPermission();
         return mTethering.getTetheredDhcpRanges();
@@ -2799,10 +3130,7 @@
                 Settings.Global.TETHER_SUPPORTED, defaultVal) != 0)
                 && !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING);
         return tetherEnabledInSettings && mUserManager.isAdminUser() &&
-                ((mTethering.getTetherableUsbRegexs().length != 0 ||
-                mTethering.getTetherableWifiRegexs().length != 0 ||
-                mTethering.getTetherableBluetoothRegexs().length != 0) &&
-                mTethering.getUpstreamIfaceTypes().length != 0);
+               mTethering.hasTetherableConfiguration();
     }
 
     @Override
@@ -2841,12 +3169,14 @@
     }
 
     // 100 percent is full good, 0 is full bad.
+    @Override
     public void reportInetCondition(int networkType, int percentage) {
         NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
         if (nai == null) return;
         reportNetworkConnectivity(nai.network, percentage > 50);
     }
 
+    @Override
     public void reportNetworkConnectivity(Network network, boolean hasConnectivity) {
         enforceAccessPermission();
         enforceInternetPermission();
@@ -2891,6 +3221,7 @@
         }
     }
 
+    @Override
     public ProxyInfo getProxyForNetwork(Network network) {
         if (network == null) return getDefaultProxy();
         final ProxyInfo globalProxy = getGlobalProxy();
@@ -3462,118 +3793,6 @@
         return -1;
     }
 
-    private static final String NOTIFICATION_ID = "CaptivePortal.Notification";
-    private static enum NotificationType { SIGN_IN, NO_INTERNET; };
-
-    private void setProvNotificationVisible(boolean visible, int networkType, String action) {
-        Intent intent = new Intent(action);
-        PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
-        // Concatenate the range of types onto the range of NetIDs.
-        int id = MAX_NET_ID + 1 + (networkType - ConnectivityManager.TYPE_NONE);
-        setProvNotificationVisibleIntent(visible, id, NotificationType.SIGN_IN,
-                networkType, null, pendingIntent, false);
-    }
-
-    /**
-     * Show or hide network provisioning notifications.
-     *
-     * We use notifications for two purposes: to notify that a network requires sign in
-     * (NotificationType.SIGN_IN), or to notify that a network does not have Internet access
-     * (NotificationType.NO_INTERNET). We display at most one notification per ID, so on a
-     * particular network we can display the notification type that was most recently requested.
-     * So for example if a captive portal fails to reply within a few seconds of connecting, we
-     * might first display NO_INTERNET, and then when the captive portal check completes, display
-     * SIGN_IN.
-     *
-     * @param id an identifier that uniquely identifies this notification.  This must match
-     *         between show and hide calls.  We use the NetID value but for legacy callers
-     *         we concatenate the range of types with the range of NetIDs.
-     */
-    private void setProvNotificationVisibleIntent(boolean visible, int id,
-            NotificationType notifyType, int networkType, String extraInfo, PendingIntent intent,
-            boolean highPriority) {
-        if (VDBG || (DBG && visible)) {
-            log("setProvNotificationVisibleIntent " + notifyType + " visible=" + visible
-                    + " networkType=" + getNetworkTypeName(networkType)
-                    + " extraInfo=" + extraInfo + " highPriority=" + highPriority);
-        }
-
-        Resources r = Resources.getSystem();
-        NotificationManager notificationManager = (NotificationManager) mContext
-            .getSystemService(Context.NOTIFICATION_SERVICE);
-
-        if (visible) {
-            CharSequence title;
-            CharSequence details;
-            int icon;
-            if (notifyType == NotificationType.NO_INTERNET &&
-                    networkType == ConnectivityManager.TYPE_WIFI) {
-                title = r.getString(R.string.wifi_no_internet, 0);
-                details = r.getString(R.string.wifi_no_internet_detailed);
-                icon = R.drawable.stat_notify_wifi_in_range;  // TODO: Need new icon.
-            } else if (notifyType == NotificationType.SIGN_IN) {
-                switch (networkType) {
-                    case ConnectivityManager.TYPE_WIFI:
-                        title = r.getString(R.string.wifi_available_sign_in, 0);
-                        details = r.getString(R.string.network_available_sign_in_detailed,
-                                extraInfo);
-                        icon = R.drawable.stat_notify_wifi_in_range;
-                        break;
-                    case ConnectivityManager.TYPE_MOBILE:
-                    case ConnectivityManager.TYPE_MOBILE_HIPRI:
-                        title = r.getString(R.string.network_available_sign_in, 0);
-                        // TODO: Change this to pull from NetworkInfo once a printable
-                        // name has been added to it
-                        details = mTelephonyManager.getNetworkOperatorName();
-                        icon = R.drawable.stat_notify_rssi_in_range;
-                        break;
-                    default:
-                        title = r.getString(R.string.network_available_sign_in, 0);
-                        details = r.getString(R.string.network_available_sign_in_detailed,
-                                extraInfo);
-                        icon = R.drawable.stat_notify_rssi_in_range;
-                        break;
-                }
-            } else {
-                Slog.wtf(TAG, "Unknown notification type " + notifyType + "on network type "
-                        + getNetworkTypeName(networkType));
-                return;
-            }
-
-            Notification notification = new Notification.Builder(mContext)
-                    .setWhen(0)
-                    .setSmallIcon(icon)
-                    .setAutoCancel(true)
-                    .setTicker(title)
-                    .setColor(mContext.getColor(
-                            com.android.internal.R.color.system_notification_accent_color))
-                    .setContentTitle(title)
-                    .setContentText(details)
-                    .setContentIntent(intent)
-                    .setLocalOnly(true)
-                    .setPriority(highPriority ?
-                            Notification.PRIORITY_HIGH :
-                            Notification.PRIORITY_DEFAULT)
-                    .setDefaults(highPriority ? Notification.DEFAULT_ALL : 0)
-                    .setOnlyAlertOnce(true)
-                    .build();
-
-            try {
-                notificationManager.notifyAsUser(NOTIFICATION_ID, id, notification, UserHandle.ALL);
-            } catch (NullPointerException npe) {
-                loge("setNotificationVisible: visible notificationManager npe=" + npe);
-                npe.printStackTrace();
-            }
-        } else {
-            try {
-                notificationManager.cancelAsUser(NOTIFICATION_ID, id, UserHandle.ALL);
-            } catch (NullPointerException npe) {
-                loge("setNotificationVisible: cancel notificationManager npe=" + npe);
-                npe.printStackTrace();
-            }
-        }
-    }
-
     /** Location to an updatable file listing carrier provisioning urls.
      *  An example:
      *
@@ -3677,7 +3896,9 @@
         enforceConnectivityInternalPermission();
         final long ident = Binder.clearCallingIdentity();
         try {
-            setProvNotificationVisible(visible, networkType, action);
+            // Concatenate the range of types onto the range of NetIDs.
+            int id = MAX_NET_ID + 1 + (networkType - ConnectivityManager.TYPE_NONE);
+            mNotifier.setProvNotificationVisible(visible, id, action);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -3806,31 +4027,12 @@
         }
     }
 
-    /**
-     * A NetworkRequest as registered by an application can be one of three
-     * types:
-     *
-     *     - "listen", for which the framework will issue callbacks about any
-     *       and all networks that match the specified NetworkCapabilities,
-     *
-     *     - "request", capable of causing a specific network to be created
-     *       first (e.g. a telephony DUN request), the framework will issue
-     *       callbacks about the single, highest scoring current network
-     *       (if any) that matches the specified NetworkCapabilities, or
-     *
-     *     - "track the default network", a hybrid of the two designed such
-     *       that the framework will issue callbacks for the single, highest
-     *       scoring current network (if any) that matches the capabilities of
-     *       the default Internet request (mDefaultRequest), but which cannot
-     *       cause the framework to either create or retain the existence of
-     *       any specific network.
-     *
-     */
-    private static enum NetworkRequestType {
-        LISTEN,
-        TRACK_DEFAULT,
-        REQUEST
-    };
+    private void ensureNetworkRequestHasType(NetworkRequest request) {
+        if (request.type == NetworkRequest.Type.NONE) {
+            throw new IllegalArgumentException(
+                    "All NetworkRequests in ConnectivityService must have a type");
+        }
+    }
 
     /**
      * Tracks info about the requester.
@@ -3844,27 +4046,26 @@
         final int mPid;
         final int mUid;
         final Messenger messenger;
-        private final NetworkRequestType mType;
 
-        NetworkRequestInfo(NetworkRequest r, PendingIntent pi, NetworkRequestType type) {
+        NetworkRequestInfo(NetworkRequest r, PendingIntent pi) {
             request = r;
+            ensureNetworkRequestHasType(request);
             mPendingIntent = pi;
             messenger = null;
             mBinder = null;
             mPid = getCallingPid();
             mUid = getCallingUid();
-            mType = type;
             enforceRequestCountLimit();
         }
 
-        NetworkRequestInfo(Messenger m, NetworkRequest r, IBinder binder, NetworkRequestType type) {
+        NetworkRequestInfo(Messenger m, NetworkRequest r, IBinder binder) {
             super();
             messenger = m;
             request = r;
+            ensureNetworkRequestHasType(request);
             mBinder = binder;
             mPid = getCallingPid();
             mUid = getCallingUid();
-            mType = type;
             mPendingIntent = null;
             enforceRequestCountLimit();
 
@@ -3885,16 +4086,6 @@
             }
         }
 
-        private String typeString() {
-            switch (mType) {
-                case LISTEN: return "Listen";
-                case REQUEST: return "Request";
-                case TRACK_DEFAULT: return "Track default";
-                default:
-                    return "unknown type";
-            }
-        }
-
         void unlinkDeathRecipient() {
             if (mBinder != null) {
                 mBinder.unlinkToDeath(this, 0);
@@ -3907,29 +4098,8 @@
             releaseNetworkRequest(request);
         }
 
-        /**
-         * Returns true iff. the contained NetworkRequest is one that:
-         *
-         *     - should be associated with at most one satisfying network
-         *       at a time;
-         *
-         *     - should cause a network to be kept up if it is the only network
-         *       which can satisfy the NetworkReqeust.
-         *
-         * For full detail of how isRequest() is used for pairing Networks with
-         * NetworkRequests read rematchNetworkAndRequests().
-         *
-         * TODO: Rename to something more properly descriptive.
-         */
-        public boolean isRequest() {
-            return (mType == NetworkRequestType.TRACK_DEFAULT) ||
-                   (mType == NetworkRequestType.REQUEST);
-        }
-
         public String toString() {
-            return typeString() +
-                    " from uid/pid:" + mUid + "/" + mPid +
-                    " for " + request +
+            return "uid/pid:" + mUid + "/" + mPid + " " + request +
                     (mPendingIntent == null ? "" : " to trigger " + mPendingIntent);
         }
     }
@@ -3979,20 +4149,23 @@
     @Override
     public NetworkRequest requestNetwork(NetworkCapabilities networkCapabilities,
             Messenger messenger, int timeoutMs, IBinder binder, int legacyType) {
-        final NetworkRequestType type = (networkCapabilities == null)
-                ? NetworkRequestType.TRACK_DEFAULT
-                : NetworkRequestType.REQUEST;
+        final NetworkRequest.Type type = (networkCapabilities == null)
+                ? NetworkRequest.Type.TRACK_DEFAULT
+                : NetworkRequest.Type.REQUEST;
         // If the requested networkCapabilities is null, take them instead from
         // the default network request. This allows callers to keep track of
         // the system default network.
-        if (type == NetworkRequestType.TRACK_DEFAULT) {
+        if (type == NetworkRequest.Type.TRACK_DEFAULT) {
             networkCapabilities = new NetworkCapabilities(mDefaultRequest.networkCapabilities);
             enforceAccessPermission();
         } else {
             networkCapabilities = new NetworkCapabilities(networkCapabilities);
             enforceNetworkRequestPermissions(networkCapabilities);
+            // TODO: this is incorrect. We mark the request as metered or not depending on the state
+            // of the app when the request is filed, but we never change the request if the app
+            // changes network state. http://b/29964605
+            enforceMeteredApnPolicy(networkCapabilities);
         }
-        enforceMeteredApnPolicy(networkCapabilities);
         ensureRequestableCapabilities(networkCapabilities);
 
         if (timeoutMs < 0 || timeoutMs > ConnectivityManager.MAX_NETWORK_REQUEST_TIMEOUT_MS) {
@@ -4006,8 +4179,8 @@
         }
 
         NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, legacyType,
-                nextNetworkRequestId());
-        NetworkRequestInfo nri = new NetworkRequestInfo(messenger, networkRequest, binder, type);
+                nextNetworkRequestId(), type);
+        NetworkRequestInfo nri = new NetworkRequestInfo(messenger, networkRequest, binder);
         if (DBG) log("requestNetwork for " + nri);
 
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_REQUEST, nri));
@@ -4020,7 +4193,7 @@
 
     private void enforceNetworkRequestPermissions(NetworkCapabilities networkCapabilities) {
         if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) == false) {
-            enforceConnectivityInternalPermission();
+            enforceConnectivityRestrictedNetworksPermission();
         } else {
             enforceChangePermission();
         }
@@ -4077,9 +4250,8 @@
         ensureRequestableCapabilities(networkCapabilities);
 
         NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, TYPE_NONE,
-                nextNetworkRequestId());
-        NetworkRequestInfo nri = new NetworkRequestInfo(networkRequest, operation,
-                NetworkRequestType.REQUEST);
+                nextNetworkRequestId(), NetworkRequest.Type.REQUEST);
+        NetworkRequestInfo nri = new NetworkRequestInfo(networkRequest, operation);
         if (DBG) log("pendingRequest for " + nri);
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_REQUEST_WITH_INTENT,
                 nri));
@@ -4128,10 +4300,19 @@
             enforceAccessPermission();
         }
 
-        NetworkRequest networkRequest = new NetworkRequest(
-                new NetworkCapabilities(networkCapabilities), TYPE_NONE, nextNetworkRequestId());
-        NetworkRequestInfo nri = new NetworkRequestInfo(messenger, networkRequest, binder,
-                NetworkRequestType.LISTEN);
+        NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
+        if (!ConnectivityManager.checkChangePermission(mContext)) {
+            // Apps without the CHANGE_NETWORK_STATE permission can't use background networks, so
+            // make all their listens include NET_CAPABILITY_FOREGROUND. That way, they will get
+            // onLost and onAvailable callbacks when networks move in and out of the background.
+            // There is no need to do this for requests because an app without CHANGE_NETWORK_STATE
+            // can't request networks.
+            nc.addCapability(NET_CAPABILITY_FOREGROUND);
+        }
+
+        NetworkRequest networkRequest = new NetworkRequest(nc, TYPE_NONE, nextNetworkRequestId(),
+                NetworkRequest.Type.LISTEN);
+        NetworkRequestInfo nri = new NetworkRequestInfo(messenger, networkRequest, binder);
         if (VDBG) log("listenForNetwork for " + nri);
 
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_LISTENER, nri));
@@ -4147,18 +4328,35 @@
         }
 
         NetworkRequest networkRequest = new NetworkRequest(
-                new NetworkCapabilities(networkCapabilities), TYPE_NONE, nextNetworkRequestId());
-        NetworkRequestInfo nri = new NetworkRequestInfo(networkRequest, operation,
-                NetworkRequestType.LISTEN);
+                new NetworkCapabilities(networkCapabilities), TYPE_NONE, nextNetworkRequestId(),
+                NetworkRequest.Type.LISTEN);
+        NetworkRequestInfo nri = new NetworkRequestInfo(networkRequest, operation);
         if (VDBG) log("pendingListenForNetwork for " + nri);
 
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_LISTENER, nri));
     }
 
     @Override
+    public void requestLinkProperties(NetworkRequest networkRequest) {
+        ensureNetworkRequestHasType(networkRequest);
+        if (networkRequest.type == NetworkRequest.Type.LISTEN) return;
+        mHandler.sendMessage(mHandler.obtainMessage(
+                EVENT_REQUEST_LINKPROPERTIES, getCallingUid(), 0, networkRequest));
+    }
+
+    @Override
+    public void requestNetworkCapabilities(NetworkRequest networkRequest) {
+        ensureNetworkRequestHasType(networkRequest);
+        if (networkRequest.type == NetworkRequest.Type.LISTEN) return;
+        mHandler.sendMessage(mHandler.obtainMessage(
+                EVENT_REQUEST_NETCAPABILITIES, getCallingUid(), 0, networkRequest));
+    }
+
+    @Override
     public void releaseNetworkRequest(NetworkRequest networkRequest) {
-        mHandler.sendMessage(mHandler.obtainMessage(EVENT_RELEASE_NETWORK_REQUEST, getCallingUid(),
-                0, networkRequest));
+        ensureNetworkRequestHasType(networkRequest);
+        mHandler.sendMessage(mHandler.obtainMessage(
+                EVENT_RELEASE_NETWORK_REQUEST, getCallingUid(), 0, networkRequest));
     }
 
     @Override
@@ -4233,6 +4431,10 @@
         return nai == getDefaultNetwork();
     }
 
+    private boolean isDefaultRequest(NetworkRequestInfo nri) {
+        return nri.request.requestId == mDefaultRequest.requestId;
+    }
+
     public int registerNetworkAgent(Messenger messenger, NetworkInfo networkInfo,
             LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
             int currentScore, NetworkMisc networkMisc) {
@@ -4401,26 +4603,18 @@
         } catch (Exception e) {
             loge("Exception in setDnsConfigurationForNetwork: " + e);
         }
-        final NetworkAgentInfo defaultNai = getDefaultNetwork();
-        if (defaultNai != null && defaultNai.network.netId == netId) {
-            setDefaultDnsSystemProperties(dnses);
-        }
         flushVmDnsCache();
     }
 
-    private void setDefaultDnsSystemProperties(Collection<InetAddress> dnses) {
-        int last = 0;
-        for (InetAddress dns : dnses) {
-            ++last;
-            String key = "net.dns" + last;
-            String value = dns.getHostAddress();
-            SystemProperties.set(key, value);
+    private String getNetworkPermission(NetworkCapabilities nc) {
+        // TODO: make these permission strings AIDL constants instead.
+        if (!nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
+            return NetworkManagementService.PERMISSION_SYSTEM;
         }
-        for (int i = last + 1; i <= mNumDnsEntries; ++i) {
-            String key = "net.dns" + i;
-            SystemProperties.set(key, "");
+        if (!nc.hasCapability(NET_CAPABILITY_FOREGROUND)) {
+            return NetworkManagementService.PERMISSION_NETWORK;
         }
-        mNumDnsEntries = last;
+        return null;
     }
 
     /**
@@ -4428,10 +4622,19 @@
      * augmented with any stateful capabilities implied from {@code networkAgent}
      * (e.g., validated status and captive portal status).
      *
+     * @param oldScore score of the network before any of the changes that prompted us
+     *                 to call this function.
      * @param nai the network having its capabilities updated.
      * @param networkCapabilities the new network capabilities.
      */
-    private void updateCapabilities(NetworkAgentInfo nai, NetworkCapabilities networkCapabilities) {
+    private void updateCapabilities(
+            int oldScore, NetworkAgentInfo nai, NetworkCapabilities networkCapabilities) {
+        if (nai.everConnected && !nai.networkCapabilities.equalImmutableCapabilities(
+                networkCapabilities)) {
+            Slog.wtf(TAG, "BUG: " + nai + " changed immutable capabilities: "
+                    + nai.networkCapabilities + " -> " + networkCapabilities);
+        }
+
         // Don't modify caller's NetworkCapabilities.
         networkCapabilities = new NetworkCapabilities(networkCapabilities);
         if (nai.lastValidated) {
@@ -4444,31 +4647,48 @@
         } else {
             networkCapabilities.removeCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
         }
-        if (!Objects.equals(nai.networkCapabilities, networkCapabilities)) {
-            final int oldScore = nai.getCurrentScore();
-            if (nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) !=
-                    networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
-                try {
-                    mNetd.setNetworkPermission(nai.network.netId,
-                            networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) ?
-                                    null : NetworkManagementService.PERMISSION_SYSTEM);
-                } catch (RemoteException e) {
-                    loge("Exception in setNetworkPermission: " + e);
-                }
+        if (nai.isBackgroundNetwork()) {
+            networkCapabilities.removeCapability(NET_CAPABILITY_FOREGROUND);
+        } else {
+            networkCapabilities.addCapability(NET_CAPABILITY_FOREGROUND);
+        }
+
+        if (Objects.equals(nai.networkCapabilities, networkCapabilities)) return;
+
+        final String oldPermission = getNetworkPermission(nai.networkCapabilities);
+        final String newPermission = getNetworkPermission(networkCapabilities);
+        if (!Objects.equals(oldPermission, newPermission) && nai.created && !nai.isVPN()) {
+            try {
+                mNetd.setNetworkPermission(nai.network.netId, newPermission);
+            } catch (RemoteException e) {
+                loge("Exception in setNetworkPermission: " + e);
             }
-            synchronized (nai) {
-                nai.networkCapabilities = networkCapabilities;
-            }
+        }
+
+        final NetworkCapabilities prevNc = nai.networkCapabilities;
+        synchronized (nai) {
+            nai.networkCapabilities = networkCapabilities;
+        }
+        if (nai.getCurrentScore() == oldScore &&
+                networkCapabilities.equalRequestableCapabilities(prevNc)) {
+            // If the requestable capabilities haven't changed, and the score hasn't changed, then
+            // the change we're processing can't affect any requests, it can only affect the listens
+            // on this network. We might have been called by rematchNetworkAndRequests when a
+            // network changed foreground state.
+            processListenRequests(nai, true);
+        } else {
+            // If the requestable capabilities have changed or the score changed, we can't have been
+            // called by rematchNetworkAndRequests, so it's safe to start a rematch.
             rematchAllNetworksAndRequests(nai, oldScore);
             notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
         }
     }
 
     private void sendUpdatedScoreToFactories(NetworkAgentInfo nai) {
-        for (int i = 0; i < nai.networkRequests.size(); i++) {
-            NetworkRequest nr = nai.networkRequests.valueAt(i);
+        for (int i = 0; i < nai.numNetworkRequests(); i++) {
+            NetworkRequest nr = nai.requestAt(i);
             // Don't send listening requests to factories. b/17393458
-            if (!isRequest(nr)) continue;
+            if (nr.isListen()) continue;
             sendUpdatedScoreToFactories(nr, nai.getCurrentScore());
         }
     }
@@ -4517,7 +4737,7 @@
     }
 
     private void callCallbackForRequest(NetworkRequestInfo nri,
-            NetworkAgentInfo networkAgent, int notificationType) {
+            NetworkAgentInfo networkAgent, int notificationType, int arg1) {
         if (nri.messenger == null) return;  // Default request has no msgr
         Bundle bundle = new Bundle();
         bundle.putParcelable(NetworkRequest.class.getSimpleName(),
@@ -4529,7 +4749,7 @@
         }
         switch (notificationType) {
             case ConnectivityManager.CALLBACK_LOSING: {
-                msg.arg1 = 30 * 1000; // TODO - read this from NetworkMonitor
+                msg.arg1 = arg1;
                 break;
             }
             case ConnectivityManager.CALLBACK_CAP_CHANGED: {
@@ -4558,12 +4778,14 @@
     }
 
     private void teardownUnneededNetwork(NetworkAgentInfo nai) {
-        for (int i = 0; i < nai.networkRequests.size(); i++) {
-            NetworkRequest nr = nai.networkRequests.valueAt(i);
-            // Ignore listening requests.
-            if (!isRequest(nr)) continue;
-            loge("Dead network still had at least " + nr);
-            break;
+        if (nai.numRequestNetworkRequests() != 0) {
+            for (int i = 0; i < nai.numNetworkRequests(); i++) {
+                NetworkRequest nr = nai.requestAt(i);
+                // Ignore listening requests.
+                if (nr.isListen()) continue;
+                loge("Dead network still had at least " + nr);
+                break;
+            }
         }
         nai.asyncChannel.disconnect();
     }
@@ -4574,7 +4796,19 @@
             return;
         }
         if (DBG) log("handleLingerComplete for " + oldNetwork.name());
-        teardownUnneededNetwork(oldNetwork);
+
+        // If we get here it means that the last linger timeout for this network expired. So there
+        // must be no other active linger timers, and we must stop lingering.
+        oldNetwork.clearLingerState();
+
+        if (unneeded(oldNetwork, UnneededFor.TEARDOWN)) {
+            // Tear the network down.
+            teardownUnneededNetwork(oldNetwork);
+        } else {
+            // Put the network in the background.
+            updateCapabilities(oldNetwork.getCurrentScore(), oldNetwork,
+                    oldNetwork.networkCapabilities);
+        }
     }
 
     private void makeDefault(NetworkAgentInfo newNetwork) {
@@ -4588,7 +4822,31 @@
         notifyLockdownVpn(newNetwork);
         handleApplyDefaultProxy(newNetwork.linkProperties.getHttpProxy());
         updateTcpBufferSizes(newNetwork);
-        setDefaultDnsSystemProperties(newNetwork.linkProperties.getDnsServers());
+    }
+
+    private void processListenRequests(NetworkAgentInfo nai, boolean capabilitiesChanged) {
+        // For consistency with previous behaviour, send onLost callbacks before onAvailable.
+        for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+            NetworkRequest nr = nri.request;
+            if (!nr.isListen()) continue;
+            if (nai.isSatisfyingRequest(nr.requestId) && !nai.satisfies(nr)) {
+                nai.removeRequest(nri.request.requestId);
+                callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_LOST, 0);
+            }
+        }
+
+        if (capabilitiesChanged) {
+            notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
+        }
+
+        for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+            NetworkRequest nr = nri.request;
+            if (!nr.isListen()) continue;
+            if (nai.satisfies(nr) && !nai.isSatisfyingRequest(nr.requestId)) {
+                nai.addRequest(nr);
+                notifyNetworkCallback(nai, nri);
+            }
+        }
     }
 
     // Handles a network appearing or improving its score.
@@ -4619,18 +4877,30 @@
     //               performed to tear down unvalidated networks that have no chance (i.e. even if
     //               validated) of becoming the highest scoring network.
     private void rematchNetworkAndRequests(NetworkAgentInfo newNetwork,
-            ReapUnvalidatedNetworks reapUnvalidatedNetworks) {
+            ReapUnvalidatedNetworks reapUnvalidatedNetworks, long now) {
         if (!newNetwork.everConnected) return;
         boolean keep = newNetwork.isVPN();
         boolean isNewDefault = false;
         NetworkAgentInfo oldDefaultNetwork = null;
+
+        final boolean wasBackgroundNetwork = newNetwork.isBackgroundNetwork();
+        final int score = newNetwork.getCurrentScore();
+
         if (VDBG) log("rematching " + newNetwork.name());
+
         // Find and migrate to this Network any NetworkRequests for
         // which this network is now the best.
         ArrayList<NetworkAgentInfo> affectedNetworks = new ArrayList<NetworkAgentInfo>();
         ArrayList<NetworkRequestInfo> addedRequests = new ArrayList<NetworkRequestInfo>();
-        if (VDBG) log(" network has: " + newNetwork.networkCapabilities);
+        NetworkCapabilities nc = newNetwork.networkCapabilities;
+        if (VDBG) log(" network has: " + nc);
         for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+            // Process requests in the first pass and listens in the second pass. This allows us to
+            // change a network's capabilities depending on which requests it has. This is only
+            // correct if the change in capabilities doesn't affect whether the network satisfies
+            // requests or not, and doesn't affect the network's score.
+            if (nri.request.isListen()) continue;
+
             final NetworkAgentInfo currentNetwork = mNetworkForRequestId.get(nri.request.requestId);
             final boolean satisfies = newNetwork.satisfies(nri.request);
             if (newNetwork == currentNetwork && satisfies) {
@@ -4645,32 +4915,24 @@
             // check if it satisfies the NetworkCapabilities
             if (VDBG) log("  checking if request is satisfied: " + nri.request);
             if (satisfies) {
-                if (!nri.isRequest()) {
-                    // This is not a request, it's a callback listener.
-                    // Add it to newNetwork regardless of score.
-                    if (newNetwork.addRequest(nri.request)) addedRequests.add(nri);
-                    continue;
-                }
-
                 // next check if it's better than any current network we're using for
                 // this request
                 if (VDBG) {
                     log("currentScore = " +
                             (currentNetwork != null ? currentNetwork.getCurrentScore() : 0) +
-                            ", newScore = " + newNetwork.getCurrentScore());
+                            ", newScore = " + score);
                 }
-                if (currentNetwork == null ||
-                        currentNetwork.getCurrentScore() < newNetwork.getCurrentScore()) {
+                if (currentNetwork == null || currentNetwork.getCurrentScore() < score) {
                     if (VDBG) log("rematch for " + newNetwork.name());
                     if (currentNetwork != null) {
                         if (VDBG) log("   accepting network in place of " + currentNetwork.name());
-                        currentNetwork.networkRequests.remove(nri.request.requestId);
-                        currentNetwork.networkLingered.add(nri.request);
+                        currentNetwork.removeRequest(nri.request.requestId);
+                        currentNetwork.lingerRequest(nri.request, now, mLingerDelayMs);
                         affectedNetworks.add(currentNetwork);
                     } else {
                         if (VDBG) log("   accepting network in place of null");
                     }
-                    unlinger(newNetwork);
+                    newNetwork.unlingerRequest(nri.request);
                     mNetworkForRequestId.put(nri.request.requestId, newNetwork);
                     if (!newNetwork.addRequest(nri.request)) {
                         Slog.wtf(TAG, "BUG: " + newNetwork.name() + " already has " + nri.request);
@@ -4682,16 +4944,19 @@
                     // TODO - this could get expensive if we have alot of requests for this
                     // network.  Think about if there is a way to reduce this.  Push
                     // netid->request mapping to each factory?
-                    sendUpdatedScoreToFactories(nri.request, newNetwork.getCurrentScore());
-                    if (mDefaultRequest.requestId == nri.request.requestId) {
+                    sendUpdatedScoreToFactories(nri.request, score);
+                    if (isDefaultRequest(nri)) {
                         isNewDefault = true;
                         oldDefaultNetwork = currentNetwork;
+                        if (currentNetwork != null) {
+                            mLingerMonitor.noteLingerDefaultNetwork(currentNetwork, newNetwork);
+                        }
                     }
                 }
-            } else if (newNetwork.networkRequests.get(nri.request.requestId) != null) {
+            } else if (newNetwork.isSatisfyingRequest(nri.request.requestId)) {
                 // If "newNetwork" is listed as satisfying "nri" but no longer satisfies "nri",
                 // mark it as no longer satisfying "nri".  Because networks are processed by
-                // rematchAllNetworkAndRequests() in descending score order, "currentNetwork" will
+                // rematchAllNetworksAndRequests() in descending score order, "currentNetwork" will
                 // match "newNetwork" before this loop will encounter a "currentNetwork" with higher
                 // score than "newNetwork" and where "currentNetwork" no longer satisfies "nri".
                 // This means this code doesn't have to handle the case where "currentNetwork" no
@@ -4700,41 +4965,23 @@
                     log("Network " + newNetwork.name() + " stopped satisfying" +
                             " request " + nri.request.requestId);
                 }
-                newNetwork.networkRequests.remove(nri.request.requestId);
+                newNetwork.removeRequest(nri.request.requestId);
                 if (currentNetwork == newNetwork) {
                     mNetworkForRequestId.remove(nri.request.requestId);
                     sendUpdatedScoreToFactories(nri.request, 0);
                 } else {
-                    if (nri.isRequest()) {
-                        Slog.wtf(TAG, "BUG: Removing request " + nri.request.requestId + " from " +
-                                newNetwork.name() +
-                                " without updating mNetworkForRequestId or factories!");
-                    }
+                    Slog.wtf(TAG, "BUG: Removing request " + nri.request.requestId + " from " +
+                            newNetwork.name() +
+                            " without updating mNetworkForRequestId or factories!");
                 }
-                // TODO: technically, sending CALLBACK_LOST here is
-                // incorrect if nri is a request (not a listen) and there
-                // is a replacement network currently connected that can
-                // satisfy it. However, the only capability that can both
+                // TODO: Technically, sending CALLBACK_LOST here is
+                // incorrect if there is a replacement network currently
+                // connected that can satisfy nri, which is a request
+                // (not a listen). However, the only capability that can both
                 // a) be requested and b) change is NET_CAPABILITY_TRUSTED,
                 // so this code is only incorrect for a network that loses
                 // the TRUSTED capability, which is a rare case.
-                callCallbackForRequest(nri, newNetwork, ConnectivityManager.CALLBACK_LOST);
-            }
-        }
-        // Linger any networks that are no longer needed.
-        for (NetworkAgentInfo nai : affectedNetworks) {
-            if (nai.lingering) {
-                // Already lingered.  Nothing to do.  This can only happen if "nai" is in
-                // "affectedNetworks" twice.  The reasoning being that to get added to
-                // "affectedNetworks", "nai" must have been satisfying a NetworkRequest
-                // (i.e. not lingered) so it could have only been lingered by this loop.
-                // unneeded(nai) will be false and we'll call unlinger() below which would
-                // be bad, so handle it here.
-            } else if (unneeded(nai)) {
-                linger(nai);
-            } else {
-                // Clear nai.networkLingered we might have added above.
-                unlinger(nai);
+                callCallbackForRequest(nri, newNetwork, ConnectivityManager.CALLBACK_LOST, 0);
             }
         }
         if (isNewDefault) {
@@ -4755,10 +5002,41 @@
             }
         }
 
+        if (!newNetwork.networkCapabilities.equalRequestableCapabilities(nc)) {
+            Slog.wtf(TAG, String.format(
+                    "BUG: %s changed requestable capabilities during rematch: %s -> %s",
+                    nc, newNetwork.networkCapabilities));
+        }
+        if (newNetwork.getCurrentScore() != score) {
+            Slog.wtf(TAG, String.format(
+                    "BUG: %s changed score during rematch: %d -> %d",
+                    score, newNetwork.getCurrentScore()));
+        }
+
+        // Second pass: process all listens.
+        if (wasBackgroundNetwork != newNetwork.isBackgroundNetwork()) {
+            // If the network went from background to foreground or vice versa, we need to update
+            // its foreground state. It is safe to do this after rematching the requests because
+            // NET_CAPABILITY_FOREGROUND does not affect requests, as is not a requestable
+            // capability and does not affect the network's score (see the Slog.wtf call above).
+            updateCapabilities(score, newNetwork, newNetwork.networkCapabilities);
+        } else {
+            processListenRequests(newNetwork, false);
+        }
+
         // do this after the default net is switched, but
         // before LegacyTypeTracker sends legacy broadcasts
         for (NetworkRequestInfo nri : addedRequests) notifyNetworkCallback(newNetwork, nri);
 
+        // Linger any networks that are no longer needed. This should be done after sending the
+        // available callback for newNetwork.
+        for (NetworkAgentInfo nai : affectedNetworks) {
+            updateLingerState(nai, now);
+        }
+        // Possibly unlinger newNetwork. Unlingering a network does not send any callbacks so it
+        // does not need to be done in any particular order.
+        updateLingerState(newNetwork, now);
+
         if (isNewDefault) {
             // Maintain the illusion: since the legacy API only
             // understands one network at a time, we must pretend
@@ -4805,9 +5083,9 @@
             // (notification callbacks) and then uses the old api (getNetworkInfo(type))
             // they may get old info.  Reverse this after the old startUsing api is removed.
             // This is on top of the multiple intent sequencing referenced in the todo above.
-            for (int i = 0; i < newNetwork.networkRequests.size(); i++) {
-                NetworkRequest nr = newNetwork.networkRequests.valueAt(i);
-                if (nr.legacyType != TYPE_NONE && isRequest(nr)) {
+            for (int i = 0; i < newNetwork.numNetworkRequests(); i++) {
+                NetworkRequest nr = newNetwork.requestAt(i);
+                if (nr.legacyType != TYPE_NONE && nr.isRequest()) {
                     // legacy type tracker filters out repeat adds
                     mLegacyTypeTracker.add(nr.legacyType, newNetwork);
                 }
@@ -4823,9 +5101,20 @@
         }
         if (reapUnvalidatedNetworks == ReapUnvalidatedNetworks.REAP) {
             for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
-                if (unneeded(nai)) {
-                    if (DBG) log("Reaping " + nai.name());
-                    teardownUnneededNetwork(nai);
+                if (unneeded(nai, UnneededFor.TEARDOWN)) {
+                    if (nai.getLingerExpiry() > 0) {
+                        // This network has active linger timers and no requests, but is not
+                        // lingering. Linger it.
+                        //
+                        // One way (the only way?) this can happen if this network is unvalidated
+                        // and became unneeded due to another network improving its score to the
+                        // point where this network will no longer be able to satisfy any requests
+                        // even if it validates.
+                        updateLingerState(nai, now);
+                    } else {
+                        if (DBG) log("Reaping " + nai.name());
+                        teardownUnneededNetwork(nai);
+                    }
                 }
             }
         }
@@ -4852,8 +5141,9 @@
         // Optimization: Only reprocess "changed" if its score improved.  This is safe because it
         // can only add more NetworkRequests satisfied by "changed", and this is exactly what
         // rematchNetworkAndRequests() handles.
+        final long now = SystemClock.elapsedRealtime();
         if (changed != null && oldScore < changed.getCurrentScore()) {
-            rematchNetworkAndRequests(changed, ReapUnvalidatedNetworks.REAP);
+            rematchNetworkAndRequests(changed, ReapUnvalidatedNetworks.REAP, now);
         } else {
             final NetworkAgentInfo[] nais = mNetworkAgentInfos.values().toArray(
                     new NetworkAgentInfo[mNetworkAgentInfos.size()]);
@@ -4867,7 +5157,8 @@
                         // is complete could incorrectly teardown a network that hasn't yet been
                         // rematched.
                         (nai != nais[nais.length-1]) ? ReapUnvalidatedNetworks.DONT_REAP
-                                : ReapUnvalidatedNetworks.REAP);
+                                : ReapUnvalidatedNetworks.REAP,
+                        now);
             }
         }
     }
@@ -4924,6 +5215,10 @@
         if (!networkAgent.created
                 && (state == NetworkInfo.State.CONNECTED
                 || (state == NetworkInfo.State.CONNECTING && networkAgent.isVPN()))) {
+
+            // A network that has just connected has zero requests and is thus a foreground network.
+            networkAgent.networkCapabilities.addCapability(NET_CAPABILITY_FOREGROUND);
+
             try {
                 // This should never fail.  Specifying an already in use NetID will cause failure.
                 if (networkAgent.isVPN()) {
@@ -4933,9 +5228,7 @@
                                 !networkAgent.networkMisc.allowBypass));
                 } else {
                     mNetd.createPhysicalNetwork(networkAgent.network.netId,
-                            networkAgent.networkCapabilities.hasCapability(
-                                    NET_CAPABILITY_NOT_RESTRICTED) ?
-                                    null : NetworkManagementService.PERMISSION_SYSTEM);
+                            getNetworkPermission(networkAgent.networkCapabilities));
                 }
             } catch (Exception e) {
                 loge("Error creating network " + networkAgent.network.netId + ": "
@@ -4977,7 +5270,8 @@
             updateSignalStrengthThresholds(networkAgent, "CONNECT", null);
 
             // Consider network even though it is not yet validated.
-            rematchNetworkAndRequests(networkAgent, ReapUnvalidatedNetworks.REAP);
+            final long now = SystemClock.elapsedRealtime();
+            rematchNetworkAndRequests(networkAgent, ReapUnvalidatedNetworks.REAP, now);
 
             // This has to happen after matching the requests, because callbacks are just requests.
             notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_PRECHECK);
@@ -5025,14 +5319,9 @@
     // notify only this one new request of the current state
     protected void notifyNetworkCallback(NetworkAgentInfo nai, NetworkRequestInfo nri) {
         int notifyType = ConnectivityManager.CALLBACK_AVAILABLE;
-        // TODO - read state from monitor to decide what to send.
-//        if (nai.networkMonitor.isLingering()) {
-//            notifyType = NetworkCallbacks.LOSING;
-//        } else if (nai.networkMonitor.isEvaluating()) {
-//            notifyType = NetworkCallbacks.callCallbackForRequest(request, nai, notifyType);
-//        }
+        mHandler.removeMessages(EVENT_TIMEOUT_NETWORK_REQUEST, nri);
         if (nri.mPendingIntent == null) {
-            callCallbackForRequest(nri, nai, notifyType);
+            callCallbackForRequest(nri, nai, notifyType, 0);
         } else {
             sendPendingIntentForRequest(nri, nai, notifyType);
         }
@@ -5066,7 +5355,7 @@
                 intent.putExtra(ConnectivityManager.EXTRA_EXTRA_INFO, info.getExtraInfo());
             }
             NetworkAgentInfo newDefaultAgent = null;
-            if (nai.networkRequests.get(mDefaultRequest.requestId) != null) {
+            if (nai.isSatisfyingRequest(mDefaultRequest.requestId)) {
                 newDefaultAgent = getDefaultNetwork();
                 if (newDefaultAgent != null) {
                     intent.putExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO,
@@ -5084,20 +5373,26 @@
         }
     }
 
-    protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType) {
+    protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType, int arg1) {
         if (VDBG) log("notifyType " + notifyTypeToName(notifyType) + " for " + networkAgent.name());
-        for (int i = 0; i < networkAgent.networkRequests.size(); i++) {
-            NetworkRequest nr = networkAgent.networkRequests.valueAt(i);
+        for (int i = 0; i < networkAgent.numNetworkRequests(); i++) {
+            NetworkRequest nr = networkAgent.requestAt(i);
             NetworkRequestInfo nri = mNetworkRequests.get(nr);
             if (VDBG) log(" sending notification for " + nr);
+            // TODO: if we're in the middle of a rematch, can we send a CAP_CHANGED callback for
+            // a network that no longer satisfies the listen?
             if (nri.mPendingIntent == null) {
-                callCallbackForRequest(nri, networkAgent, notifyType);
+                callCallbackForRequest(nri, networkAgent, notifyType, arg1);
             } else {
                 sendPendingIntentForRequest(nri, networkAgent, notifyType);
             }
         }
     }
 
+    protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType) {
+        notifyNetworkCallbacks(networkAgent, notifyType, 0);
+    }
+
     private String notifyTypeToName(int notifyType) {
         switch (notifyType) {
             case ConnectivityManager.CALLBACK_PRECHECK:    return "PRECHECK";
@@ -5157,7 +5452,7 @@
 
     @Override
     public String getCaptivePortalServerUrl() {
-        return NetworkMonitor.getCaptivePortalServerUrl(mContext);
+        return NetworkMonitor.getCaptivePortalServerHttpUrl(mContext);
     }
 
     @Override
@@ -5232,6 +5527,9 @@
                 }
             }
         }
+
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.NETWORK_AVOID_BAD_WIFI, null);
     }
 
     @VisibleForTesting
@@ -5240,7 +5538,17 @@
         return new NetworkMonitor(context, handler, nai, defaultRequest);
     }
 
-    private static void logDefaultNetworkEvent(NetworkAgentInfo newNai, NetworkAgentInfo prevNai) {
+    @VisibleForTesting
+    AvoidBadWifiTracker createAvoidBadWifiTracker(Context c, Handler h, Runnable r) {
+        return new AvoidBadWifiTracker(c, h, r);
+    }
+
+    @VisibleForTesting
+    public WakeupMessage makeWakeupMessage(Context c, Handler h, String s, int cmd, Object obj) {
+        return new WakeupMessage(c, h, s, cmd, 0, 0, obj);
+    }
+
+    private void logDefaultNetworkEvent(NetworkAgentInfo newNai, NetworkAgentInfo prevNai) {
         int newNetid = NETID_UNSET;
         int prevNetid = NETID_UNSET;
         int[] transports = new int[0];
@@ -5258,6 +5566,10 @@
             hadIPv6 = lp.hasGlobalIPv6Address() && lp.hasIPv6DefaultRoute();
         }
 
-        DefaultNetworkEvent.logEvent(newNetid, transports, prevNetid, hadIPv4, hadIPv6);
+        mMetricsLog.log(new DefaultNetworkEvent(newNetid, transports, prevNetid, hadIPv4, hadIPv6));
+    }
+
+    private void logNetworkEvent(NetworkAgentInfo nai, int evtype) {
+        mMetricsLog.log(new NetworkEvent(nai.network.netId, evtype));
     }
 }
diff --git a/services/core/java/com/android/server/connectivity/LingerMonitor.java b/services/core/java/com/android/server/connectivity/LingerMonitor.java
new file mode 100644
index 0000000..635db19
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/LingerMonitor.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2016 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.connectivity;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.NetworkCapabilities;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.util.SparseBooleanArray;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.MessageUtils;
+import com.android.server.connectivity.NetworkNotificationManager;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+
+import static android.net.ConnectivityManager.NETID_UNSET;
+
+/**
+ * Class that monitors default network linger events and possibly notifies the user of network
+ * switches.
+ *
+ * This class is not thread-safe and all its methods must be called on the ConnectivityService
+ * handler thread.
+ */
+public class LingerMonitor {
+
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;
+    private static final String TAG = LingerMonitor.class.getSimpleName();
+
+    public static final int DEFAULT_NOTIFICATION_DAILY_LIMIT = 3;
+    public static final long DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS = DateUtils.MINUTE_IN_MILLIS;
+
+    private static final HashMap<String, Integer> TRANSPORT_NAMES = makeTransportToNameMap();
+    @VisibleForTesting
+    public static final Intent CELLULAR_SETTINGS = new Intent().setComponent(new ComponentName(
+            "com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity"));
+
+    @VisibleForTesting
+    public static final int NOTIFY_TYPE_NONE         = 0;
+    public static final int NOTIFY_TYPE_NOTIFICATION = 1;
+    public static final int NOTIFY_TYPE_TOAST        = 2;
+
+    private static SparseArray<String> sNotifyTypeNames = MessageUtils.findMessageNames(
+            new Class[] { LingerMonitor.class }, new String[]{ "NOTIFY_TYPE_" });
+
+    private final Context mContext;
+    private final NetworkNotificationManager mNotifier;
+    private final int mDailyLimit;
+    private final long mRateLimitMillis;
+
+    private long mFirstNotificationMillis;
+    private long mLastNotificationMillis;
+    private int mNotificationCounter;
+
+    /** Current notifications. Maps the netId we switched away from to the netId we switched to. */
+    private final SparseIntArray mNotifications = new SparseIntArray();
+
+    /** Whether we ever notified that we switched away from a particular network. */
+    private final SparseBooleanArray mEverNotified = new SparseBooleanArray();
+
+    public LingerMonitor(Context context, NetworkNotificationManager notifier,
+            int dailyLimit, long rateLimitMillis) {
+        mContext = context;
+        mNotifier = notifier;
+        mDailyLimit = dailyLimit;
+        mRateLimitMillis = rateLimitMillis;
+    }
+
+    private static HashMap<String, Integer> makeTransportToNameMap() {
+        SparseArray<String> numberToName = MessageUtils.findMessageNames(
+            new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" });
+        HashMap<String, Integer> nameToNumber = new HashMap<>();
+        for (int i = 0; i < numberToName.size(); i++) {
+            // MessageUtils will fail to initialize if there are duplicate constant values, so there
+            // are no duplicates here.
+            nameToNumber.put(numberToName.valueAt(i), numberToName.keyAt(i));
+        }
+        return nameToNumber;
+    }
+
+    private static boolean hasTransport(NetworkAgentInfo nai, int transport) {
+        return nai.networkCapabilities.hasTransport(transport);
+    }
+
+    private int getNotificationSource(NetworkAgentInfo toNai) {
+        for (int i = 0; i < mNotifications.size(); i++) {
+            if (mNotifications.valueAt(i) == toNai.network.netId) {
+                return mNotifications.keyAt(i);
+            }
+        }
+        return NETID_UNSET;
+    }
+
+    private boolean everNotified(NetworkAgentInfo nai) {
+        return mEverNotified.get(nai.network.netId, false);
+    }
+
+    @VisibleForTesting
+    public boolean isNotificationEnabled(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+        // TODO: Evaluate moving to CarrierConfigManager.
+        String[] notifySwitches =
+                mContext.getResources().getStringArray(R.array.config_networkNotifySwitches);
+
+        if (VDBG) {
+            Log.d(TAG, "Notify on network switches: " + Arrays.toString(notifySwitches));
+        }
+
+        for (String notifySwitch : notifySwitches) {
+            if (TextUtils.isEmpty(notifySwitch)) continue;
+            String[] transports = notifySwitch.split("-", 2);
+            if (transports.length != 2) {
+                Log.e(TAG, "Invalid network switch notification configuration: " + notifySwitch);
+                continue;
+            }
+            int fromTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[0]);
+            int toTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[1]);
+            if (hasTransport(fromNai, fromTransport) && hasTransport(toNai, toTransport)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private void showNotification(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+        mNotifier.showNotification(fromNai.network.netId, NotificationType.NETWORK_SWITCH,
+                fromNai, toNai, createNotificationIntent(), true);
+    }
+
+    @VisibleForTesting
+    protected PendingIntent createNotificationIntent() {
+        return PendingIntent.getActivityAsUser(mContext, 0, CELLULAR_SETTINGS,
+                PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
+    }
+
+    // Removes any notification that was put up as a result of switching to nai.
+    private void maybeStopNotifying(NetworkAgentInfo nai) {
+        int fromNetId = getNotificationSource(nai);
+        if (fromNetId != NETID_UNSET) {
+            mNotifications.delete(fromNetId);
+            mNotifier.clearNotification(fromNetId);
+            // Toasts can't be deleted.
+        }
+    }
+
+    // Notify the user of a network switch using a notification or a toast.
+    private void notify(NetworkAgentInfo fromNai, NetworkAgentInfo toNai, boolean forceToast) {
+        int notifyType =
+                mContext.getResources().getInteger(R.integer.config_networkNotifySwitchType);
+        if (notifyType == NOTIFY_TYPE_NOTIFICATION && forceToast) {
+            notifyType = NOTIFY_TYPE_TOAST;
+        }
+
+        if (VDBG) {
+            Log.d(TAG, "Notify type: " + sNotifyTypeNames.get(notifyType, "" + notifyType));
+        }
+
+        switch (notifyType) {
+            case NOTIFY_TYPE_NONE:
+                return;
+            case NOTIFY_TYPE_NOTIFICATION:
+                showNotification(fromNai, toNai);
+                break;
+            case NOTIFY_TYPE_TOAST:
+                mNotifier.showToast(fromNai, toNai);
+                break;
+            default:
+                Log.e(TAG, "Unknown notify type " + notifyType);
+                return;
+        }
+
+        if (DBG) {
+            Log.d(TAG, "Notifying switch from=" + fromNai.name() + " to=" + toNai.name() +
+                    " type=" + sNotifyTypeNames.get(notifyType, "unknown(" + notifyType + ")"));
+        }
+
+        mNotifications.put(fromNai.network.netId, toNai.network.netId);
+        mEverNotified.put(fromNai.network.netId, true);
+    }
+
+    // The default network changed from fromNai to toNai due to a change in score.
+    public void noteLingerDefaultNetwork(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+        if (VDBG) {
+            Log.d(TAG, "noteLingerDefaultNetwork from=" + fromNai.name() +
+                    " everValidated=" + fromNai.everValidated +
+                    " lastValidated=" + fromNai.lastValidated +
+                    " to=" + toNai.name());
+        }
+
+        // If we are currently notifying the user because the device switched to fromNai, now that
+        // we are switching away from it we should remove the notification. This includes the case
+        // where we switch back to toNai because its score improved again (e.g., because it regained
+        // Internet access).
+        maybeStopNotifying(fromNai);
+
+        // If this network never validated, don't notify. Otherwise, we could do things like:
+        //
+        // 1. Unvalidated wifi connects.
+        // 2. Unvalidated mobile data connects.
+        // 3. Cell validates, and we show a notification.
+        // or:
+        // 1. User connects to wireless printer.
+        // 2. User turns on cellular data.
+        // 3. We show a notification.
+        if (!fromNai.everValidated) return;
+
+        // If this network is a captive portal, don't notify. This cannot happen on initial connect
+        // to a captive portal, because the everValidated check above will fail. However, it can
+        // happen if the captive portal reasserts itself (e.g., because its timeout fires). In that
+        // case, as soon as the captive portal reasserts itself, we'll show a sign-in notification.
+        // We don't want to overwrite that notification with this one; the user has already been
+        // notified, and of the two, the captive portal notification is the more useful one because
+        // it allows the user to sign in to the captive portal. In this case, display a toast
+        // in addition to the captive portal notification.
+        //
+        // Note that if the network we switch to is already up when the captive portal reappears,
+        // this won't work because NetworkMonitor tells ConnectivityService that the network is
+        // unvalidated (causing a switch) before asking it to show the sign in notification. In this
+        // case, the toast won't show and we'll only display the sign in notification. This is the
+        // best we can do at this time.
+        boolean forceToast = fromNai.networkCapabilities.hasCapability(
+                NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL);
+
+        // Only show the notification once, in order to avoid irritating the user every time.
+        // TODO: should we do this?
+        if (everNotified(fromNai)) {
+            if (VDBG) {
+                Log.d(TAG, "Not notifying handover from " + fromNai.name() + ", already notified");
+            }
+            return;
+        }
+
+        // Only show the notification if we switched away because a network became unvalidated, not
+        // because its score changed.
+        // TODO: instead of just skipping notification, keep a note of it, and show it if it becomes
+        // unvalidated.
+        if (fromNai.lastValidated) return;
+
+        if (!isNotificationEnabled(fromNai, toNai)) return;
+
+        final long now = SystemClock.elapsedRealtime();
+        if (isRateLimited(now) || isAboveDailyLimit(now)) return;
+
+        notify(fromNai, toNai, forceToast);
+    }
+
+    public void noteDisconnect(NetworkAgentInfo nai) {
+        mNotifications.delete(nai.network.netId);
+        mEverNotified.delete(nai.network.netId);
+        maybeStopNotifying(nai);
+        // No need to cancel notifications on nai: NetworkMonitor does that on disconnect.
+    }
+
+    private boolean isRateLimited(long now) {
+        final long millisSinceLast = now - mLastNotificationMillis;
+        if (millisSinceLast < mRateLimitMillis) {
+            return true;
+        }
+        mLastNotificationMillis = now;
+        return false;
+    }
+
+    private boolean isAboveDailyLimit(long now) {
+        if (mFirstNotificationMillis == 0) {
+            mFirstNotificationMillis = now;
+        }
+        final long millisSinceFirst = now - mFirstNotificationMillis;
+        if (millisSinceFirst > DateUtils.DAY_IN_MILLIS) {
+            mNotificationCounter = 0;
+            mFirstNotificationMillis = 0;
+        }
+        if (mNotificationCounter >= mDailyLimit) {
+            return true;
+        }
+        mNotificationCounter++;
+        return false;
+    }
+}
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index d487bd0..2a618bc 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -28,14 +28,21 @@
 import android.net.NetworkState;
 import android.os.Handler;
 import android.os.Messenger;
+import android.os.SystemClock;
+import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.internal.util.AsyncChannel;
+import com.android.internal.util.WakeupMessage;
 import com.android.server.ConnectivityService;
 import com.android.server.connectivity.NetworkMonitor;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.Objects;
+import java.util.SortedSet;
+import java.util.TreeSet;
 
 /**
  * A bag class used by ConnectivityService for holding a collection of most recent
@@ -97,15 +104,18 @@
 // -----------------------------------------------
 // If a network has no chance of satisfying any requests (even if it were to become validated
 // and enter state #5), ConnectivityService will disconnect the NetworkAgent's AsyncChannel.
-// If the network ever for any period of time had satisfied a NetworkRequest (i.e. had been
-// the highest scoring that satisfied the NetworkRequest's constraints), but is no longer the
-// highest scoring network for any NetworkRequest, then there will be a 30s pause before
-// ConnectivityService disconnects the NetworkAgent's AsyncChannel.  During this pause the
-// network is considered "lingering".  This pause exists to allow network communication to be
-// wrapped up rather than abruptly terminated.  During this pause if the network begins satisfying
-// a NetworkRequest, ConnectivityService will cancel the future disconnection of the NetworkAgent's
-// AsyncChannel, and the network is no longer considered "lingering".
+//
+// If the network was satisfying a foreground NetworkRequest (i.e. had been the highest scoring that
+// satisfied the NetworkRequest's constraints), but is no longer the highest scoring network for any
+// foreground NetworkRequest, then there will be a 30s pause to allow network communication to be
+// wrapped up rather than abruptly terminated. During this pause the network is said to be
+// "lingering". During this pause if the network begins satisfying a foreground NetworkRequest,
+// ConnectivityService will cancel the future disconnection of the NetworkAgent's AsyncChannel, and
+// the network is no longer considered "lingering". After the linger timer expires, if the network
+// is satisfying one or more background NetworkRequests it is kept up in the background. If it is
+// not, ConnectivityService disconnects the NetworkAgent's AsyncChannel.
 public class NetworkAgentInfo implements Comparable<NetworkAgentInfo> {
+
     public NetworkInfo networkInfo;
     // This Network object should always be used if possible, so as to encourage reuse of the
     // enclosed socket factory and connection pool.  Avoid creating other Network objects.
@@ -130,12 +140,14 @@
     public boolean everValidated;
 
     // The result of the last validation attempt on this network (true if validated, false if not).
-    // This bit exists only because we never unvalidate a network once it's been validated, and that
-    // is because the network scoring and revalidation code does not (may not?) deal properly with
-    // networks becoming unvalidated.
-    // TODO: Fix the network scoring code, remove this, and rename everValidated to validated.
     public boolean lastValidated;
 
+    // If true, becoming unvalidated will lower the network's score. This is only meaningful if the
+    // system is configured not to do this for certain networks, e.g., if the
+    // config_networkAvoidBadWifi option is set to 0 and the user has not overridden that via
+    // Settings.Global.NETWORK_AVOID_BAD_WIFI.
+    public boolean avoidUnvalidated;
+
     // Whether a captive portal was ever detected on this network.
     // This is a sticky bit; once set it is never cleared.
     public boolean everCaptivePortalDetected;
@@ -143,12 +155,69 @@
     // Whether a captive portal was found during the last network validation attempt.
     public boolean lastCaptivePortalDetected;
 
-    // Indicates whether the network is lingering.  Networks are lingered when they become unneeded
-    // as a result of their NetworkRequests being satisfied by a different network, so as to allow
-    // communication to wrap up before the network is taken down.  This usually only happens to the
-    // default network.  Lingering ends with either the linger timeout expiring and the network
-    // being taken down, or the network satisfying a request again.
-    public boolean lingering;
+    // Networks are lingered when they become unneeded as a result of their NetworkRequests being
+    // satisfied by a higher-scoring network. so as to allow communication to wrap up before the
+    // network is taken down.  This usually only happens to the default network. Lingering ends with
+    // either the linger timeout expiring and the network being taken down, or the network
+    // satisfying a request again.
+    public static class LingerTimer implements Comparable<LingerTimer> {
+        public final NetworkRequest request;
+        public final long expiryMs;
+
+        public LingerTimer(NetworkRequest request, long expiryMs) {
+            this.request = request;
+            this.expiryMs = expiryMs;
+        }
+        public boolean equals(Object o) {
+            if (!(o instanceof LingerTimer)) return false;
+            LingerTimer other = (LingerTimer) o;
+            return (request.requestId == other.request.requestId) && (expiryMs == other.expiryMs);
+        }
+        public int hashCode() {
+            return Objects.hash(request.requestId, expiryMs);
+        }
+        public int compareTo(LingerTimer other) {
+            return (expiryMs != other.expiryMs) ?
+                    Long.compare(expiryMs, other.expiryMs) :
+                    Integer.compare(request.requestId, other.request.requestId);
+        }
+        public String toString() {
+            return String.format("%s, expires %dms", request.toString(),
+                    expiryMs - SystemClock.elapsedRealtime());
+        }
+    }
+
+    /**
+     * Inform ConnectivityService that the network LINGER period has
+     * expired.
+     * obj = this NetworkAgentInfo
+     */
+    public static final int EVENT_NETWORK_LINGER_COMPLETE = 1001;
+
+    // All linger timers for this network, sorted by expiry time. A linger timer is added whenever
+    // a request is moved to a network with a better score, regardless of whether the network is or
+    // was lingering or not.
+    // TODO: determine if we can replace this with a smaller or unsorted data structure. (e.g.,
+    // SparseLongArray) combined with the timestamp of when the last timer is scheduled to fire.
+    private final SortedSet<LingerTimer> mLingerTimers = new TreeSet<>();
+
+    // For fast lookups. Indexes into mLingerTimers by request ID.
+    private final SparseArray<LingerTimer> mLingerTimerForRequest = new SparseArray<>();
+
+    // Linger expiry timer. Armed whenever mLingerTimers is non-empty, regardless of whether the
+    // network is lingering or not. Always set to the expiry of the LingerTimer that expires last.
+    // When the timer fires, all linger state is cleared, and if the network has no requests, it is
+    // torn down.
+    private WakeupMessage mLingerMessage;
+
+    // Linger expiry. Holds the expiry time of the linger timer, or 0 if the timer is not armed.
+    private long mLingerExpiryMs;
+
+    // Whether the network is lingering or not. Must be maintained separately from the above because
+    // it depends on the state of other networks and requests, which only ConnectivityService knows.
+    // (Example: we don't linger a network if it would become the best for a NetworkRequest if it
+    // validated).
+    private boolean mLingering;
 
     // This represents the last score received from the NetworkAgent.
     private int currentScore;
@@ -162,11 +231,13 @@
     private static final int MAXIMUM_NETWORK_SCORE = 100;
 
     // The list of NetworkRequests being satisfied by this Network.
-    public final SparseArray<NetworkRequest> networkRequests = new SparseArray<NetworkRequest>();
-    // The list of NetworkRequests that this Network previously satisfied with the highest
-    // score.  A non-empty list indicates that if this Network was validated it is lingered.
-    // NOTE: This list is only used for debugging.
-    public final ArrayList<NetworkRequest> networkLingered = new ArrayList<NetworkRequest>();
+    private final SparseArray<NetworkRequest> mNetworkRequests = new SparseArray<>();
+
+    // How many of the satisfied requests are actual requests and not listens.
+    private int mNumRequestNetworkRequests = 0;
+
+    // How many of the satisfied requests are of type BACKGROUND_REQUEST.
+    private int mNumBackgroundNetworkRequests = 0;
 
     public final Messenger messenger;
     public final AsyncChannel asyncChannel;
@@ -174,6 +245,12 @@
     // Used by ConnectivityService to keep track of 464xlat.
     public Nat464Xlat clatd;
 
+    private static final String TAG = ConnectivityService.class.getSimpleName();
+    private static final boolean VDBG = false;
+    private final ConnectivityService mConnService;
+    private final Context mContext;
+    private final Handler mHandler;
+
     public NetworkAgentInfo(Messenger messenger, AsyncChannel ac, Network net, NetworkInfo info,
             LinkProperties lp, NetworkCapabilities nc, int score, Context context, Handler handler,
             NetworkMisc misc, NetworkRequest defaultRequest, ConnectivityService connService) {
@@ -184,22 +261,131 @@
         linkProperties = lp;
         networkCapabilities = nc;
         currentScore = score;
-        networkMonitor = connService.createNetworkMonitor(context, handler, this, defaultRequest);
+        mConnService = connService;
+        mContext = context;
+        mHandler = handler;
+        networkMonitor = mConnService.createNetworkMonitor(context, handler, this, defaultRequest);
         networkMisc = misc;
     }
 
+    // Functions for manipulating the requests satisfied by this network.
+    //
+    // These functions must only called on ConnectivityService's main thread.
+
+    private static final boolean ADD = true;
+    private static final boolean REMOVE = false;
+
+    private void updateRequestCounts(boolean add, NetworkRequest request) {
+        int delta = add ? +1 : -1;
+        switch (request.type) {
+            case REQUEST:
+            case TRACK_DEFAULT:
+                mNumRequestNetworkRequests += delta;
+                break;
+
+            case BACKGROUND_REQUEST:
+                mNumRequestNetworkRequests += delta;
+                mNumBackgroundNetworkRequests += delta;
+                break;
+
+            case LISTEN:
+                break;
+
+            case NONE:
+            default:
+                Log.wtf(TAG, "Unhandled request type " + request.type);
+                break;
+        }
+    }
+
     /**
      * Add {@code networkRequest} to this network as it's satisfied by this network.
-     * NOTE: This function must only be called on ConnectivityService's main thread.
      * @return true if {@code networkRequest} was added or false if {@code networkRequest} was
      *         already present.
      */
     public boolean addRequest(NetworkRequest networkRequest) {
-        if (networkRequests.get(networkRequest.requestId) == networkRequest) return false;
-        networkRequests.put(networkRequest.requestId, networkRequest);
+        NetworkRequest existing = mNetworkRequests.get(networkRequest.requestId);
+        if (existing == networkRequest) return false;
+        if (existing != null) {
+            // Should only happen if the requestId wraps. If that happens lots of other things will
+            // be broken as well.
+            Log.wtf(TAG, String.format("Duplicate requestId for %s and %s on %s",
+                    networkRequest, existing, name()));
+            updateRequestCounts(REMOVE, existing);
+        }
+        mNetworkRequests.put(networkRequest.requestId, networkRequest);
+        updateRequestCounts(ADD, networkRequest);
         return true;
     }
 
+    /**
+     * Remove the specified request from this network.
+     */
+    public void removeRequest(int requestId) {
+        NetworkRequest existing = mNetworkRequests.get(requestId);
+        if (existing == null) return;
+        updateRequestCounts(REMOVE, existing);
+        mNetworkRequests.remove(requestId);
+        if (existing.isRequest()) {
+            unlingerRequest(existing);
+        }
+    }
+
+    /**
+     * Returns whether this network is currently satisfying the request with the specified ID.
+     */
+    public boolean isSatisfyingRequest(int id) {
+        return mNetworkRequests.get(id) != null;
+    }
+
+    /**
+     * Returns the request at the specified position in the list of requests satisfied by this
+     * network.
+     */
+    public NetworkRequest requestAt(int index) {
+        return mNetworkRequests.valueAt(index);
+    }
+
+    /**
+     * Returns the number of requests currently satisfied by this network for which
+     * {@link android.net.NetworkRequest#isRequest} returns {@code true}.
+     */
+    public int numRequestNetworkRequests() {
+        return mNumRequestNetworkRequests;
+    }
+
+    /**
+     * Returns the number of requests currently satisfied by this network of type
+     * {@link android.net.NetworkRequest.Type.BACKGROUND_REQUEST}.
+     */
+    public int numBackgroundNetworkRequests() {
+        return mNumBackgroundNetworkRequests;
+    }
+
+    /**
+     * Returns the number of foreground requests currently satisfied by this network.
+     */
+    public int numForegroundNetworkRequests() {
+        return mNumRequestNetworkRequests - mNumBackgroundNetworkRequests;
+    }
+
+    /**
+     * Returns the number of requests of any type currently satisfied by this network.
+     */
+    public int numNetworkRequests() {
+        return mNetworkRequests.size();
+    }
+
+    /**
+     * Returns whether the network is a background network. A network is a background network if it
+     * is satisfying no foreground requests and at least one background request. (If it did not have
+     * a background request, it would be a speculative network that is only being kept up because
+     * it might satisfy a request if it validated).
+     */
+    public boolean isBackgroundNetwork() {
+        return !isVPN() && numForegroundNetworkRequests() == 0 && mNumBackgroundNetworkRequests > 0;
+    }
+
     // Does this network satisfy request?
     public boolean satisfies(NetworkRequest request) {
         return created &&
@@ -232,16 +418,22 @@
         }
 
         int score = currentScore;
-        // Use NET_CAPABILITY_VALIDATED here instead of lastValidated, this allows
-        // ConnectivityService.updateCapabilities() to compute the old score prior to updating
-        // networkCapabilities (with a potentially different validated state).
-        if (!networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) && !pretendValidated) {
+        if (!lastValidated && !pretendValidated && !ignoreWifiUnvalidationPenalty()) {
             score -= UNVALIDATED_SCORE_PENALTY;
         }
         if (score < 0) score = 0;
         return score;
     }
 
+    // Return true on devices configured to ignore score penalty for wifi networks
+    // that become unvalidated (b/31075769).
+    private boolean ignoreWifiUnvalidationPenalty() {
+        boolean isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
+                networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        boolean avoidBadWifi = mConnService.avoidBadWifi() || avoidUnvalidated;
+        return isWifi && !avoidBadWifi && everValidated;
+    }
+
     // Get the current score for this Network.  This may be modified from what the
     // NetworkAgent sent, as it has modifiers applied to it.
     public int getCurrentScore() {
@@ -269,13 +461,103 @@
         }
     }
 
+    /**
+     * Sets the specified request to linger on this network for the specified time. Called by
+     * ConnectivityService when the request is moved to another network with a higher score.
+     */
+    public void lingerRequest(NetworkRequest request, long now, long duration) {
+        if (mLingerTimerForRequest.get(request.requestId) != null) {
+            // Cannot happen. Once a request is lingering on a particular network, we cannot
+            // re-linger it unless that network becomes the best for that request again, in which
+            // case we should have unlingered it.
+            Log.wtf(TAG, this.name() + ": request " + request.requestId + " already lingered");
+        }
+        final long expiryMs = now + duration;
+        LingerTimer timer = new LingerTimer(request, expiryMs);
+        if (VDBG) Log.d(TAG, "Adding LingerTimer " + timer + " to " + this.name());
+        mLingerTimers.add(timer);
+        mLingerTimerForRequest.put(request.requestId, timer);
+    }
+
+    /**
+     * Cancel lingering. Called by ConnectivityService when a request is added to this network.
+     * Returns true if the given request was lingering on this network, false otherwise.
+     */
+    public boolean unlingerRequest(NetworkRequest request) {
+        LingerTimer timer = mLingerTimerForRequest.get(request.requestId);
+        if (timer != null) {
+            if (VDBG) Log.d(TAG, "Removing LingerTimer " + timer + " from " + this.name());
+            mLingerTimers.remove(timer);
+            mLingerTimerForRequest.remove(request.requestId);
+            return true;
+        }
+        return false;
+    }
+
+    public long getLingerExpiry() {
+        return mLingerExpiryMs;
+    }
+
+    public void updateLingerTimer() {
+        long newExpiry = mLingerTimers.isEmpty() ? 0 : mLingerTimers.last().expiryMs;
+        if (newExpiry == mLingerExpiryMs) return;
+
+        // Even if we're going to reschedule the timer, cancel it first. This is because the
+        // semantics of WakeupMessage guarantee that if cancel is called then the alarm will
+        // never call its callback (handleLingerComplete), even if it has already fired.
+        // WakeupMessage makes no such guarantees about rescheduling a message, so if mLingerMessage
+        // has already been dispatched, rescheduling to some time in the future it won't stop it
+        // from calling its callback immediately.
+        if (mLingerMessage != null) {
+            mLingerMessage.cancel();
+            mLingerMessage = null;
+        }
+
+        if (newExpiry > 0) {
+            mLingerMessage = mConnService.makeWakeupMessage(
+                    mContext, mHandler,
+                    "NETWORK_LINGER_COMPLETE." + network.netId,
+                    EVENT_NETWORK_LINGER_COMPLETE, this);
+            mLingerMessage.schedule(newExpiry);
+        }
+
+        mLingerExpiryMs = newExpiry;
+    }
+
+    public void linger() {
+        mLingering = true;
+    }
+
+    public void unlinger() {
+        mLingering = false;
+    }
+
+    public boolean isLingering() {
+        return mLingering;
+    }
+
+    public void clearLingerState() {
+        if (mLingerMessage != null) {
+            mLingerMessage.cancel();
+            mLingerMessage = null;
+        }
+        mLingerTimers.clear();
+        mLingerTimerForRequest.clear();
+        updateLingerTimer();  // Sets mLingerExpiryMs, cancels and nulls out mLingerMessage.
+        mLingering = false;
+    }
+
+    public void dumpLingerTimers(PrintWriter pw) {
+        for (LingerTimer timer : mLingerTimers) { pw.println(timer); }
+    }
+
     public String toString() {
         return "NetworkAgentInfo{ ni{" + networkInfo + "}  " +
                 "network{" + network + "}  nethandle{" + network.getNetworkHandle() + "}  " +
                 "lp{" + linkProperties + "}  " +
                 "nc{" + networkCapabilities + "}  Score{" + getCurrentScore() + "}  " +
                 "everValidated{" + everValidated + "}  lastValidated{" + lastValidated + "}  " +
-                "created{" + created + "} lingering{" + lingering + "} " +
+                "created{" + created + "} lingering{" + isLingering() + "} " +
                 "explicitlySelected{" + networkMisc.explicitlySelected + "} " +
                 "acceptUnvalidated{" + networkMisc.acceptUnvalidated + "} " +
                 "everCaptivePortalDetected{" + everCaptivePortalDetected + "} " +
diff --git a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
new file mode 100644
index 0000000..9ffa40b
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2016 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.connectivity;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.widget.Toast;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.NetworkCapabilities;
+import android.os.UserHandle;
+import android.telephony.TelephonyManager;
+import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.R;
+
+import static android.net.NetworkCapabilities.*;
+
+
+public class NetworkNotificationManager {
+
+    public static enum NotificationType { SIGN_IN, NO_INTERNET, LOST_INTERNET, NETWORK_SWITCH };
+
+    @VisibleForTesting
+    static final String NOTIFICATION_ID = "Connectivity.Notification";
+
+    private static final String TAG = NetworkNotificationManager.class.getSimpleName();
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;
+
+    private final Context mContext;
+    private final TelephonyManager mTelephonyManager;
+    private final NotificationManager mNotificationManager;
+
+    public NetworkNotificationManager(Context c, TelephonyManager t, NotificationManager n) {
+        mContext = c;
+        mTelephonyManager = t;
+        mNotificationManager = n;
+    }
+
+    // TODO: deal more gracefully with multi-transport networks.
+    private static int getFirstTransportType(NetworkAgentInfo nai) {
+        for (int i = 0; i < 64; i++) {
+            if (nai.networkCapabilities.hasTransport(i)) return i;
+        }
+        return -1;
+    }
+
+    private static String getTransportName(int transportType) {
+        Resources r = Resources.getSystem();
+        String[] networkTypes = r.getStringArray(R.array.network_switch_type_name);
+        try {
+            return networkTypes[transportType];
+        } catch (IndexOutOfBoundsException e) {
+            return r.getString(R.string.network_switch_type_name_unknown);
+        }
+    }
+
+    private static int getIcon(int transportType) {
+        return (transportType == TRANSPORT_WIFI) ?
+                R.drawable.stat_notify_wifi_in_range :  // TODO: Distinguish ! from ?.
+                R.drawable.stat_notify_rssi_in_range;
+    }
+
+    /**
+     * Show or hide network provisioning notifications.
+     *
+     * We use notifications for two purposes: to notify that a network requires sign in
+     * (NotificationType.SIGN_IN), or to notify that a network does not have Internet access
+     * (NotificationType.NO_INTERNET). We display at most one notification per ID, so on a
+     * particular network we can display the notification type that was most recently requested.
+     * So for example if a captive portal fails to reply within a few seconds of connecting, we
+     * might first display NO_INTERNET, and then when the captive portal check completes, display
+     * SIGN_IN.
+     *
+     * @param id an identifier that uniquely identifies this notification.  This must match
+     *         between show and hide calls.  We use the NetID value but for legacy callers
+     *         we concatenate the range of types with the range of NetIDs.
+     * @param nai the network with which the notification is associated. For a SIGN_IN, NO_INTERNET,
+     *         or LOST_INTERNET notification, this is the network we're connecting to. For a
+     *         NETWORK_SWITCH notification it's the network that we switched from. When this network
+     *         disconnects the notification is removed.
+     * @param switchToNai for a NETWORK_SWITCH notification, the network we are switching to. Null
+     *         in all other cases. Only used to determine the text of the notification.
+     */
+    public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
+            NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) {
+        int transportType;
+        String extraInfo;
+        if (nai != null) {
+            transportType = getFirstTransportType(nai);
+            extraInfo = nai.networkInfo.getExtraInfo();
+            // Only notify for Internet-capable networks.
+            if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) return;
+        } else {
+            // Legacy notifications.
+            transportType = TRANSPORT_CELLULAR;
+            extraInfo = null;
+        }
+
+        if (DBG) {
+            Slog.d(TAG, "showNotification id=" + id + " " + notifyType
+                    + " transportType=" + getTransportName(transportType)
+                    + " extraInfo=" + extraInfo + " highPriority=" + highPriority);
+        }
+
+        Resources r = Resources.getSystem();
+        CharSequence title;
+        CharSequence details;
+        int icon = getIcon(transportType);
+        if (notifyType == NotificationType.NO_INTERNET && transportType == TRANSPORT_WIFI) {
+            title = r.getString(R.string.wifi_no_internet, 0);
+            details = r.getString(R.string.wifi_no_internet_detailed);
+        } else if (notifyType == NotificationType.LOST_INTERNET &&
+                transportType == TRANSPORT_WIFI) {
+            title = r.getString(R.string.wifi_no_internet, 0);
+            details = r.getString(R.string.wifi_no_internet_detailed);
+        } else if (notifyType == NotificationType.SIGN_IN) {
+            switch (transportType) {
+                case TRANSPORT_WIFI:
+                    title = r.getString(R.string.wifi_available_sign_in, 0);
+                    details = r.getString(R.string.network_available_sign_in_detailed, extraInfo);
+                    break;
+                case TRANSPORT_CELLULAR:
+                    title = r.getString(R.string.network_available_sign_in, 0);
+                    // TODO: Change this to pull from NetworkInfo once a printable
+                    // name has been added to it
+                    details = mTelephonyManager.getNetworkOperatorName();
+                    break;
+                default:
+                    title = r.getString(R.string.network_available_sign_in, 0);
+                    details = r.getString(R.string.network_available_sign_in_detailed, extraInfo);
+                    break;
+            }
+        } else if (notifyType == NotificationType.NETWORK_SWITCH) {
+            String fromTransport = getTransportName(transportType);
+            String toTransport = getTransportName(getFirstTransportType(switchToNai));
+            title = r.getString(R.string.network_switch_metered, toTransport);
+            details = r.getString(R.string.network_switch_metered_detail, toTransport,
+                    fromTransport);
+        } else {
+            Slog.wtf(TAG, "Unknown notification type " + notifyType + "on network transport "
+                    + getTransportName(transportType));
+            return;
+        }
+
+        Notification.Builder builder = new Notification.Builder(mContext)
+                .setWhen(System.currentTimeMillis())
+                .setShowWhen(notifyType == NotificationType.NETWORK_SWITCH)
+                .setSmallIcon(icon)
+                .setAutoCancel(true)
+                .setTicker(title)
+                .setColor(mContext.getColor(
+                        com.android.internal.R.color.system_notification_accent_color))
+                .setContentTitle(title)
+                .setContentIntent(intent)
+                .setLocalOnly(true)
+                .setPriority(highPriority ?
+                        Notification.PRIORITY_HIGH :
+                        Notification.PRIORITY_DEFAULT)
+                .setDefaults(highPriority ? Notification.DEFAULT_ALL : 0)
+                .setOnlyAlertOnce(true);
+
+        if (notifyType == NotificationType.NETWORK_SWITCH) {
+            builder.setStyle(new Notification.BigTextStyle().bigText(details));
+        } else {
+            builder.setContentText(details);
+        }
+
+        Notification notification = builder.build();
+
+        try {
+            mNotificationManager.notifyAsUser(NOTIFICATION_ID, id, notification, UserHandle.ALL);
+        } catch (NullPointerException npe) {
+            Slog.d(TAG, "setNotificationVisible: visible notificationManager error", npe);
+        }
+    }
+
+    public void clearNotification(int id) {
+        if (DBG) {
+            Slog.d(TAG, "clearNotification id=" + id);
+        }
+        try {
+            mNotificationManager.cancelAsUser(NOTIFICATION_ID, id, UserHandle.ALL);
+        } catch (NullPointerException npe) {
+            Slog.d(TAG, "setNotificationVisible: cancel notificationManager error", npe);
+        }
+    }
+
+    /**
+     * Legacy provisioning notifications coming directly from DcTracker.
+     */
+    public void setProvNotificationVisible(boolean visible, int id, String action) {
+        if (visible) {
+            Intent intent = new Intent(action);
+            PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+            showNotification(id, NotificationType.SIGN_IN, null, null, pendingIntent, false);
+        } else {
+            clearNotification(id);
+        }
+    }
+
+    public void showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+        String fromTransport = getTransportName(getFirstTransportType(fromNai));
+        String toTransport = getTransportName(getFirstTransportType(toNai));
+        String text = mContext.getResources().getString(
+                R.string.network_switch_metered_toast, fromTransport, toTransport);
+        Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
+    }
+}
diff --git a/services/core/java/com/android/server/connectivity/PermissionMonitor.java b/services/core/java/com/android/server/connectivity/PermissionMonitor.java
index eb1c77e..e084ff8 100644
--- a/services/core/java/com/android/server/connectivity/PermissionMonitor.java
+++ b/services/core/java/com/android/server/connectivity/PermissionMonitor.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.CHANGE_NETWORK_STATE;
 import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
+import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
 import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
 import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
@@ -65,10 +66,10 @@
     private final BroadcastReceiver mIntentReceiver;
 
     // Values are User IDs.
-    private final Set<Integer> mUsers = new HashSet<Integer>();
+    private final Set<Integer> mUsers = new HashSet<>();
 
     // Keys are App IDs. Values are true for SYSTEM permission and false for NETWORK permission.
-    private final Map<Integer, Boolean> mApps = new HashMap<Integer, Boolean>();
+    private final Map<Integer, Boolean> mApps = new HashMap<>();
 
     public PermissionMonitor(Context context, INetworkManagementService netd) {
         mContext = context;
@@ -126,14 +127,14 @@
             }
 
             boolean isNetwork = hasNetworkPermission(app);
-            boolean isSystem = hasSystemPermission(app);
+            boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app);
 
-            if (isNetwork || isSystem) {
+            if (isNetwork || hasRestrictedPermission) {
                 Boolean permission = mApps.get(uid);
                 // If multiple packages share a UID (cf: android:sharedUserId) and ask for different
                 // permissions, don't downgrade (i.e., if it's already SYSTEM, leave it as is).
                 if (permission == null || permission == NETWORK) {
-                    mApps.put(uid, isSystem);
+                    mApps.put(uid, hasRestrictedPermission);
                 }
             }
         }
@@ -164,12 +165,13 @@
         return hasPermission(app, CHANGE_NETWORK_STATE);
     }
 
-    private boolean hasSystemPermission(PackageInfo app) {
+    private boolean hasRestrictedNetworkPermission(PackageInfo app) {
         int flags = app.applicationInfo != null ? app.applicationInfo.flags : 0;
         if ((flags & FLAG_SYSTEM) != 0 || (flags & FLAG_UPDATED_SYSTEM_APP) != 0) {
             return true;
         }
-        return hasPermission(app, CONNECTIVITY_INTERNAL);
+        return hasPermission(app, CONNECTIVITY_INTERNAL)
+                || hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS);
     }
 
     private int[] toIntArray(List<Integer> list) {
@@ -181,8 +183,8 @@
     }
 
     private void update(Set<Integer> users, Map<Integer, Boolean> apps, boolean add) {
-        List<Integer> network = new ArrayList<Integer>();
-        List<Integer> system = new ArrayList<Integer>();
+        List<Integer> network = new ArrayList<>();
+        List<Integer> system = new ArrayList<>();
         for (Entry<Integer, Boolean> app : apps.entrySet()) {
             List<Integer> list = app.getValue() ? system : network;
             for (int user : users) {
@@ -209,7 +211,7 @@
         }
         mUsers.add(user);
 
-        Set<Integer> users = new HashSet<Integer>();
+        Set<Integer> users = new HashSet<>();
         users.add(user);
         update(users, mApps, true);
     }
@@ -221,7 +223,7 @@
         }
         mUsers.remove(user);
 
-        Set<Integer> users = new HashSet<Integer>();
+        Set<Integer> users = new HashSet<>();
         users.add(user);
         update(users, mApps, false);
     }
@@ -234,9 +236,9 @@
         try {
             final PackageInfo app = mPackageManager.getPackageInfo(name, GET_PERMISSIONS);
             final boolean isNetwork = hasNetworkPermission(app);
-            final boolean isSystem = hasSystemPermission(app);
-            if (isNetwork || isSystem) {
-                currentPermission = isSystem;
+            final boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app);
+            if (isNetwork || hasRestrictedPermission) {
+                currentPermission = hasRestrictedPermission;
             }
         } catch (NameNotFoundException e) {
             // App not found.
@@ -257,7 +259,7 @@
         if (permission != mApps.get(appUid)) {
             mApps.put(appUid, permission);
 
-            Map<Integer, Boolean> apps = new HashMap<Integer, Boolean>();
+            Map<Integer, Boolean> apps = new HashMap<>();
             apps.put(appUid, permission);
             update(mUsers, apps, true);
         }
@@ -268,7 +270,7 @@
             loge("Invalid app in onAppRemoved: " + appUid);
             return;
         }
-        Map<Integer, Boolean> apps = new HashMap<Integer, Boolean>();
+        Map<Integer, Boolean> apps = new HashMap<>();
 
         Boolean permission = null;
         String[] packages = mPackageManager.getPackagesForUid(appUid);
diff --git a/services/net/java/android/net/util/AvoidBadWifiTracker.java b/services/net/java/android/net/util/AvoidBadWifiTracker.java
new file mode 100644
index 0000000..2abaeb1
--- /dev/null
+++ b/services/net/java/android/net/util/AvoidBadWifiTracker.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2016 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.util;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.R;
+
+import static android.provider.Settings.Global.NETWORK_AVOID_BAD_WIFI;
+
+/**
+ * A class to encapsulate management of the "Smart Networking" capability of
+ * avoiding bad Wi-Fi when, for example upstream connectivity is lost or
+ * certain critical link failures occur.
+ *
+ * This enables the device to switch to another form of connectivity, like
+ * mobile, if it's available and working.
+ *
+ * The Runnable |cb|, if given, is called on the supplied Handler's thread
+ * whether the computed "avoid bad wifi" value changes.
+ *
+ * Disabling this reverts the device to a level of networking sophistication
+ * circa 2012-13 by disabling disparate code paths each of which contribute to
+ * maintaining continuous, working Internet connectivity.
+ *
+ * @hide
+ */
+public class AvoidBadWifiTracker {
+    private static String TAG = AvoidBadWifiTracker.class.getSimpleName();
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final Runnable mReevaluateRunnable;
+    private final Uri mUri;
+    private final ContentResolver mResolver;
+    private final SettingObserver mSettingObserver;
+    private final BroadcastReceiver mBroadcastReceiver;
+
+    private volatile boolean mAvoidBadWifi = true;
+
+    public AvoidBadWifiTracker(Context ctx, Handler handler) {
+        this(ctx, handler, null);
+    }
+
+    public AvoidBadWifiTracker(Context ctx, Handler handler, Runnable cb) {
+        mContext = ctx;
+        mHandler = handler;
+        mReevaluateRunnable = () -> { if (update() && cb != null) cb.run(); };
+        mUri = Settings.Global.getUriFor(NETWORK_AVOID_BAD_WIFI);
+        mResolver = mContext.getContentResolver();
+        mSettingObserver = new SettingObserver();
+        mBroadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                reevaluate();
+            }
+        };
+
+        update();
+    }
+
+    public void start() {
+        mResolver.registerContentObserver(mUri, false, mSettingObserver);
+
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+        mContext.registerReceiverAsUser(
+                mBroadcastReceiver, UserHandle.ALL, intentFilter, null, null);
+
+        reevaluate();
+    }
+
+    public void shutdown() {
+        mResolver.unregisterContentObserver(mSettingObserver);
+
+        mContext.unregisterReceiver(mBroadcastReceiver);
+    }
+
+    public boolean currentValue() {
+        return mAvoidBadWifi;
+    }
+
+    /**
+     * Whether the device or carrier configuration disables avoiding bad wifi by default.
+     */
+    public boolean configRestrictsAvoidBadWifi() {
+        return (mContext.getResources().getInteger(R.integer.config_networkAvoidBadWifi) == 0);
+    }
+
+    /**
+     * Whether we should display a notification when wifi becomes unvalidated.
+     */
+    public boolean shouldNotifyWifiUnvalidated() {
+        return configRestrictsAvoidBadWifi() && getSettingsValue() == null;
+    }
+
+    public String getSettingsValue() {
+        return Settings.Global.getString(mResolver, NETWORK_AVOID_BAD_WIFI);
+    }
+
+    @VisibleForTesting
+    public void reevaluate() {
+        mHandler.post(mReevaluateRunnable);
+    }
+
+    public boolean update() {
+        final boolean settingAvoidBadWifi = "1".equals(getSettingsValue());
+        final boolean prev = mAvoidBadWifi;
+        mAvoidBadWifi = settingAvoidBadWifi || !configRestrictsAvoidBadWifi();
+        return mAvoidBadWifi != prev;
+    }
+
+    private class SettingObserver extends ContentObserver {
+        public SettingObserver() {
+            super(null);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            Slog.wtf(TAG, "Should never be reached.");
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            if (!mUri.equals(uri)) return;
+            reevaluate();
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
deleted file mode 100644
index 4fae4a7..0000000
--- a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
+++ /dev/null
@@ -1,1884 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server;
-
-import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
-import static android.net.ConnectivityManager.TYPE_MOBILE;
-import static android.net.ConnectivityManager.TYPE_WIFI;
-import static android.net.ConnectivityManager.getNetworkTypeName;
-import static android.net.NetworkCapabilities.*;
-
-import static org.mockito.Mockito.mock;
-
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.ConnectivityManager;
-import android.net.ConnectivityManager.NetworkCallback;
-import android.net.ConnectivityManager.PacketKeepalive;
-import android.net.ConnectivityManager.PacketKeepaliveCallback;
-import android.net.INetworkPolicyManager;
-import android.net.INetworkStatsService;
-import android.net.IpPrefix;
-import android.net.LinkAddress;
-import android.net.LinkProperties;
-import android.net.Network;
-import android.net.NetworkAgent;
-import android.net.NetworkCapabilities;
-import android.net.NetworkConfig;
-import android.net.NetworkFactory;
-import android.net.NetworkInfo;
-import android.net.NetworkInfo.DetailedState;
-import android.net.NetworkMisc;
-import android.net.NetworkRequest;
-import android.net.RouteInfo;
-import android.os.ConditionVariable;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.INetworkManagementService;
-import android.os.Looper;
-import android.os.Message;
-import android.os.MessageQueue;
-import android.os.Messenger;
-import android.os.MessageQueue.IdleHandler;
-import android.os.Process;
-import android.os.SystemClock;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.util.Log;
-import android.util.LogPrinter;
-
-import com.android.internal.util.WakeupMessage;
-import com.android.server.connectivity.NetworkAgentInfo;
-import com.android.server.connectivity.NetworkMonitor;
-import com.android.server.connectivity.NetworkMonitor.CaptivePortalProbeResult;
-import com.android.server.net.NetworkPinner;
-
-import java.net.InetAddress;
-import java.util.ArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Tests for {@link ConnectivityService}.
- *
- * Build, install and run with:
- *  runtest frameworks-services -c com.android.server.ConnectivityServiceTest
- */
-public class ConnectivityServiceTest extends AndroidTestCase {
-    private static final String TAG = "ConnectivityServiceTest";
-
-    private static final int TIMEOUT_MS = 500;
-
-    private BroadcastInterceptingContext mServiceContext;
-    private WrappedConnectivityService mService;
-    private WrappedConnectivityManager mCm;
-    private MockNetworkAgent mWiFiNetworkAgent;
-    private MockNetworkAgent mCellNetworkAgent;
-
-    // This class exists to test bindProcessToNetwork and getBoundNetworkForProcess. These methods
-    // do not go through ConnectivityService but talk to netd directly, so they don't automatically
-    // reflect the state of our test ConnectivityService.
-    private class WrappedConnectivityManager extends ConnectivityManager {
-        private Network mFakeBoundNetwork;
-
-        public synchronized boolean bindProcessToNetwork(Network network) {
-            mFakeBoundNetwork = network;
-            return true;
-        }
-
-        public synchronized Network getBoundNetworkForProcess() {
-            return mFakeBoundNetwork;
-        }
-
-        public WrappedConnectivityManager(Context context, ConnectivityService service) {
-            super(context, service);
-        }
-    }
-
-    private class MockContext extends BroadcastInterceptingContext {
-        MockContext(Context base) {
-            super(base);
-        }
-
-        @Override
-        public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
-            // PendingIntents sent by the AlarmManager are not intercepted by
-            // BroadcastInterceptingContext so we must really register the receiver.
-            // This shouldn't effect the real NetworkMonitors as the action contains a random token.
-            if (filter.getAction(0).startsWith("android.net.netmon.lingerExpired")) {
-                return getBaseContext().registerReceiver(receiver, filter);
-            } else {
-                return super.registerReceiver(receiver, filter);
-            }
-        }
-
-        @Override
-        public Object getSystemService (String name) {
-            if (name == Context.CONNECTIVITY_SERVICE) return mCm;
-            return super.getSystemService(name);
-        }
-    }
-
-    /**
-     * A subclass of HandlerThread that allows callers to wait for it to become idle. waitForIdle
-     * will return immediately if the handler is already idle.
-     */
-    private class IdleableHandlerThread extends HandlerThread {
-        private IdleHandler mIdleHandler;
-
-        public IdleableHandlerThread(String name) {
-            super(name);
-        }
-
-        public void waitForIdle(int timeoutMs) {
-            final ConditionVariable cv = new ConditionVariable();
-            final MessageQueue queue = getLooper().getQueue();
-
-            synchronized (queue) {
-                if (queue.isIdle()) {
-                    return;
-                }
-
-                assertNull("BUG: only one idle handler allowed", mIdleHandler);
-                mIdleHandler = new IdleHandler() {
-                    public boolean queueIdle() {
-                        synchronized (queue) {
-                            cv.open();
-                            mIdleHandler = null;
-                            return false;  // Remove the handler.
-                        }
-                    }
-                };
-                queue.addIdleHandler(mIdleHandler);
-            }
-
-            if (!cv.block(timeoutMs)) {
-                fail("HandlerThread " + getName() +
-                        " did not become idle after " + timeoutMs + " ms");
-                queue.removeIdleHandler(mIdleHandler);
-            }
-        }
-    }
-
-    // Tests that IdleableHandlerThread works as expected.
-    public void testIdleableHandlerThread() {
-        final int attempts = 50;  // Causes the test to take about 200ms on bullhead-eng.
-
-        // Tests that waitForIdle returns immediately if the service is already idle.
-        for (int i = 0; i < attempts; i++) {
-            mService.waitForIdle();
-        }
-
-        // Bring up a network that we can use to send messages to ConnectivityService.
-        ConditionVariable cv = waitForConnectivityBroadcasts(1);
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        mWiFiNetworkAgent.connect(false);
-        waitFor(cv);
-        Network n = mWiFiNetworkAgent.getNetwork();
-        assertNotNull(n);
-
-        // Tests that calling waitForIdle waits for messages to be processed.
-        for (int i = 0; i < attempts; i++) {
-            mWiFiNetworkAgent.setSignalStrength(i);
-            mService.waitForIdle();
-            assertEquals(i, mCm.getNetworkCapabilities(n).getSignalStrength());
-        }
-
-        // Ensure that not calling waitForIdle causes a race condition.
-        for (int i = 0; i < attempts; i++) {
-            mWiFiNetworkAgent.setSignalStrength(i);
-            if (i != mCm.getNetworkCapabilities(n).getSignalStrength()) {
-                // We hit a race condition, as expected. Pass the test.
-                return;
-            }
-        }
-
-        // No race? There is a bug in this test.
-        fail("expected race condition at least once in " + attempts + " attempts");
-    }
-
-    private class MockNetworkAgent {
-        private final WrappedNetworkMonitor mWrappedNetworkMonitor;
-        private final NetworkInfo mNetworkInfo;
-        private final NetworkCapabilities mNetworkCapabilities;
-        private final IdleableHandlerThread mHandlerThread;
-        private final ConditionVariable mDisconnected = new ConditionVariable();
-        private final ConditionVariable mNetworkStatusReceived = new ConditionVariable();
-        private int mScore;
-        private NetworkAgent mNetworkAgent;
-        private int mStartKeepaliveError = PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED;
-        private int mStopKeepaliveError = PacketKeepalive.NO_KEEPALIVE;
-        private Integer mExpectedKeepaliveSlot = null;
-        // Contains the redirectUrl from networkStatus(). Before reading, wait for
-        // mNetworkStatusReceived.
-        private String mRedirectUrl;
-
-        MockNetworkAgent(int transport) {
-            final int type = transportToLegacyType(transport);
-            final String typeName = ConnectivityManager.getNetworkTypeName(type);
-            mNetworkInfo = new NetworkInfo(type, 0, typeName, "Mock");
-            mNetworkCapabilities = new NetworkCapabilities();
-            mNetworkCapabilities.addTransportType(transport);
-            switch (transport) {
-                case TRANSPORT_WIFI:
-                    mScore = 60;
-                    break;
-                case TRANSPORT_CELLULAR:
-                    mScore = 50;
-                    break;
-                default:
-                    throw new UnsupportedOperationException("unimplemented network type");
-            }
-            mHandlerThread = new IdleableHandlerThread("Mock-" + typeName);
-            mHandlerThread.start();
-            mNetworkAgent = new NetworkAgent(mHandlerThread.getLooper(), mServiceContext,
-                    "Mock-" + typeName, mNetworkInfo, mNetworkCapabilities,
-                    new LinkProperties(), mScore, new NetworkMisc()) {
-                @Override
-                public void unwanted() { mDisconnected.open(); }
-
-                @Override
-                public void startPacketKeepalive(Message msg) {
-                    int slot = msg.arg1;
-                    if (mExpectedKeepaliveSlot != null) {
-                        assertEquals((int) mExpectedKeepaliveSlot, slot);
-                    }
-                    onPacketKeepaliveEvent(slot, mStartKeepaliveError);
-                }
-
-                @Override
-                public void stopPacketKeepalive(Message msg) {
-                    onPacketKeepaliveEvent(msg.arg1, mStopKeepaliveError);
-                }
-
-                @Override
-                public void networkStatus(int status, String redirectUrl) {
-                    mRedirectUrl = redirectUrl;
-                    mNetworkStatusReceived.open();
-                }
-            };
-            // Waits for the NetworkAgent to be registered, which includes the creation of the
-            // NetworkMonitor.
-            mService.waitForIdle();
-            mWrappedNetworkMonitor = mService.getLastCreatedWrappedNetworkMonitor();
-        }
-
-        public void waitForIdle(int timeoutMs) {
-            mHandlerThread.waitForIdle(timeoutMs);
-        }
-
-        public void waitForIdle() {
-            waitForIdle(TIMEOUT_MS);
-        }
-
-        public void adjustScore(int change) {
-            mScore += change;
-            mNetworkAgent.sendNetworkScore(mScore);
-        }
-
-        public void addCapability(int capability) {
-            mNetworkCapabilities.addCapability(capability);
-            mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
-        }
-
-        public void setSignalStrength(int signalStrength) {
-            mNetworkCapabilities.setSignalStrength(signalStrength);
-            mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
-        }
-
-        public void connectWithoutInternet() {
-            mNetworkInfo.setDetailedState(DetailedState.CONNECTED, null, null);
-            mNetworkAgent.sendNetworkInfo(mNetworkInfo);
-        }
-
-        /**
-         * Transition this NetworkAgent to CONNECTED state with NET_CAPABILITY_INTERNET.
-         * @param validated Indicate if network should pretend to be validated.
-         */
-        public void connect(boolean validated) {
-            assertEquals(mNetworkInfo.getDetailedState(), DetailedState.IDLE);
-            assertFalse(mNetworkCapabilities.hasCapability(NET_CAPABILITY_INTERNET));
-
-            NetworkCallback callback = null;
-            final ConditionVariable validatedCv = new ConditionVariable();
-            if (validated) {
-                mWrappedNetworkMonitor.gen204ProbeResult = 204;
-                NetworkRequest request = new NetworkRequest.Builder()
-                        .addTransportType(mNetworkCapabilities.getTransportTypes()[0])
-                        .build();
-                callback = new NetworkCallback() {
-                    public void onCapabilitiesChanged(Network network,
-                            NetworkCapabilities networkCapabilities) {
-                        if (network.equals(getNetwork()) &&
-                            networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED)) {
-                            validatedCv.open();
-                        }
-                    }
-                };
-                mCm.registerNetworkCallback(request, callback);
-            }
-            addCapability(NET_CAPABILITY_INTERNET);
-
-            connectWithoutInternet();
-
-            if (validated) {
-                // Wait for network to validate.
-                waitFor(validatedCv);
-                mWrappedNetworkMonitor.gen204ProbeResult = 500;
-            }
-
-            if (callback != null) mCm.unregisterNetworkCallback(callback);
-        }
-
-        public void connectWithCaptivePortal(String redirectUrl) {
-            mWrappedNetworkMonitor.gen204ProbeResult = 200;
-            mWrappedNetworkMonitor.gen204ProbeRedirectUrl = redirectUrl;
-            connect(false);
-            waitFor(new Criteria() { public boolean get() {
-                NetworkCapabilities caps = mCm.getNetworkCapabilities(getNetwork());
-                return caps != null && caps.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL);} });
-            mWrappedNetworkMonitor.gen204ProbeResult = 500;
-            mWrappedNetworkMonitor.gen204ProbeRedirectUrl = null;
-        }
-
-        public void disconnect() {
-            mNetworkInfo.setDetailedState(DetailedState.DISCONNECTED, null, null);
-            mNetworkAgent.sendNetworkInfo(mNetworkInfo);
-        }
-
-        public Network getNetwork() {
-            return new Network(mNetworkAgent.netId);
-        }
-
-        public ConditionVariable getDisconnectedCV() {
-            return mDisconnected;
-        }
-
-        public WrappedNetworkMonitor getWrappedNetworkMonitor() {
-            return mWrappedNetworkMonitor;
-        }
-
-        public void sendLinkProperties(LinkProperties lp) {
-            mNetworkAgent.sendLinkProperties(lp);
-        }
-
-        public void setStartKeepaliveError(int error) {
-            mStartKeepaliveError = error;
-        }
-
-        public void setStopKeepaliveError(int error) {
-            mStopKeepaliveError = error;
-        }
-
-        public void setExpectedKeepaliveSlot(Integer slot) {
-            mExpectedKeepaliveSlot = slot;
-        }
-
-        public String waitForRedirectUrl() {
-            assertTrue(mNetworkStatusReceived.block(TIMEOUT_MS));
-            return mRedirectUrl;
-        }
-    }
-
-    /**
-     * A NetworkFactory that allows tests to wait until any in-flight NetworkRequest add or remove
-     * operations have been processed. Before ConnectivityService can add or remove any requests,
-     * the factory must be told to expect those operations by calling expectAddRequests or
-     * expectRemoveRequests.
-     */
-    private static class MockNetworkFactory extends NetworkFactory {
-        private final ConditionVariable mNetworkStartedCV = new ConditionVariable();
-        private final ConditionVariable mNetworkStoppedCV = new ConditionVariable();
-        private final AtomicBoolean mNetworkStarted = new AtomicBoolean(false);
-
-        // Used to expect that requests be removed or added on a separate thread, without sleeping.
-        // Callers can call either expectAddRequests() or expectRemoveRequests() exactly once, then
-        // cause some other thread to add or remove requests, then call waitForRequests(). We can
-        // either expect requests to be added or removed, but not both, because CountDownLatch can
-        // only count in one direction.
-        private CountDownLatch mExpectations;
-
-        // Whether we are currently expecting requests to be added or removed. Valid only if
-        // mExpectations is non-null.
-        private boolean mExpectingAdditions;
-
-        public MockNetworkFactory(Looper looper, Context context, String logTag,
-                NetworkCapabilities filter) {
-            super(looper, context, logTag, filter);
-        }
-
-        public int getMyRequestCount() {
-            return getRequestCount();
-        }
-
-        protected void startNetwork() {
-            mNetworkStarted.set(true);
-            mNetworkStartedCV.open();
-        }
-
-        protected void stopNetwork() {
-            mNetworkStarted.set(false);
-            mNetworkStoppedCV.open();
-        }
-
-        public boolean getMyStartRequested() {
-            return mNetworkStarted.get();
-        }
-
-        public ConditionVariable getNetworkStartedCV() {
-            mNetworkStartedCV.close();
-            return mNetworkStartedCV;
-        }
-
-        public ConditionVariable getNetworkStoppedCV() {
-            mNetworkStoppedCV.close();
-            return mNetworkStoppedCV;
-        }
-
-        @Override
-        protected void handleAddRequest(NetworkRequest request, int score) {
-            // If we're expecting anything, we must be expecting additions.
-            if (mExpectations != null && !mExpectingAdditions) {
-                fail("Can't add requests while expecting requests to be removed");
-            }
-
-            // Add the request.
-            super.handleAddRequest(request, score);
-
-            // Reduce the number of request additions we're waiting for.
-            if (mExpectingAdditions) {
-                assertTrue("Added more requests than expected", mExpectations.getCount() > 0);
-                mExpectations.countDown();
-            }
-        }
-
-        @Override
-        protected void handleRemoveRequest(NetworkRequest request) {
-            // If we're expecting anything, we must be expecting removals.
-            if (mExpectations != null && mExpectingAdditions) {
-                fail("Can't remove requests while expecting requests to be added");
-            }
-
-            // Remove the request.
-            super.handleRemoveRequest(request);
-
-            // Reduce the number of request removals we're waiting for.
-            if (!mExpectingAdditions) {
-                assertTrue("Removed more requests than expected", mExpectations.getCount() > 0);
-                mExpectations.countDown();
-            }
-        }
-
-        private void assertNoExpectations() {
-            if (mExpectations != null) {
-                fail("Can't add expectation, " + mExpectations.getCount() + " already pending");
-            }
-        }
-
-        // Expects that count requests will be added.
-        public void expectAddRequests(final int count) {
-            assertNoExpectations();
-            mExpectingAdditions = true;
-            mExpectations = new CountDownLatch(count);
-        }
-
-        // Expects that count requests will be removed.
-        public void expectRemoveRequests(final int count) {
-            assertNoExpectations();
-            mExpectingAdditions = false;
-            mExpectations = new CountDownLatch(count);
-        }
-
-        // Waits for the expected request additions or removals to happen within a timeout.
-        public void waitForRequests() throws InterruptedException {
-            assertNotNull("Nothing to wait for", mExpectations);
-            mExpectations.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
-            final long count = mExpectations.getCount();
-            final String msg = count + " requests still not " +
-                    (mExpectingAdditions ? "added" : "removed") +
-                    " after " + TIMEOUT_MS + " ms";
-            assertEquals(msg, 0, count);
-            mExpectations = null;
-        }
-
-        public void waitForNetworkRequests(final int count) throws InterruptedException {
-            waitForRequests();
-            assertEquals(count, getMyRequestCount());
-        }
-    }
-
-    private class FakeWakeupMessage extends WakeupMessage {
-        private static final int UNREASONABLY_LONG_WAIT = 1000;
-
-        public FakeWakeupMessage(Context context, Handler handler, String cmdName, int cmd) {
-            super(context, handler, cmdName, cmd);
-        }
-
-        @Override
-        public void schedule(long when) {
-            long delayMs = when - SystemClock.elapsedRealtime();
-            if (delayMs < 0) delayMs = 0;
-            if (delayMs > UNREASONABLY_LONG_WAIT) {
-                fail("Attempting to send msg more than " + UNREASONABLY_LONG_WAIT +
-                        "ms into the future: " + delayMs);
-            }
-            mHandler.sendEmptyMessageDelayed(mCmd, delayMs);
-        }
-
-        @Override
-        public void cancel() {
-            mHandler.removeMessages(mCmd);
-        }
-
-        @Override
-        public void onAlarm() {
-            throw new AssertionError("Should never happen. Update this fake.");
-        }
-    }
-
-    // NetworkMonitor implementation allowing overriding of Internet connectivity probe result.
-    private class WrappedNetworkMonitor extends NetworkMonitor {
-        // HTTP response code fed back to NetworkMonitor for Internet connectivity probe.
-        public int gen204ProbeResult = 500;
-        public String gen204ProbeRedirectUrl = null;
-
-        public WrappedNetworkMonitor(Context context, Handler handler,
-            NetworkAgentInfo networkAgentInfo, NetworkRequest defaultRequest) {
-            super(context, handler, networkAgentInfo, defaultRequest);
-        }
-
-        @Override
-        protected CaptivePortalProbeResult isCaptivePortal() {
-            return new CaptivePortalProbeResult(gen204ProbeResult, gen204ProbeRedirectUrl);
-        }
-
-        @Override
-        protected WakeupMessage makeWakeupMessage(
-                Context context, Handler handler, String cmdName, int cmd) {
-            return new FakeWakeupMessage(context, handler, cmdName, cmd);
-        }
-    }
-
-    private class WrappedConnectivityService extends ConnectivityService {
-        private WrappedNetworkMonitor mLastCreatedNetworkMonitor;
-
-        public WrappedConnectivityService(Context context, INetworkManagementService netManager,
-                INetworkStatsService statsService, INetworkPolicyManager policyManager) {
-            super(context, netManager, statsService, policyManager);
-        }
-
-        @Override
-        protected HandlerThread createHandlerThread() {
-            return new IdleableHandlerThread("WrappedConnectivityService");
-        }
-
-        @Override
-        protected int getDefaultTcpRwnd() {
-            // Prevent wrapped ConnectivityService from trying to write to SystemProperties.
-            return 0;
-        }
-
-        @Override
-        protected int reserveNetId() {
-            while (true) {
-                final int netId = super.reserveNetId();
-
-                // Don't overlap test NetIDs with real NetIDs as binding sockets to real networks
-                // can have odd side-effects, like network validations succeeding.
-                final Network[] networks = ConnectivityManager.from(getContext()).getAllNetworks();
-                boolean overlaps = false;
-                for (Network network : networks) {
-                    if (netId == network.netId) {
-                        overlaps = true;
-                        break;
-                    }
-                }
-                if (overlaps) continue;
-
-                return netId;
-            }
-        }
-
-        @Override
-        public NetworkMonitor createNetworkMonitor(Context context, Handler handler,
-                NetworkAgentInfo nai, NetworkRequest defaultRequest) {
-            final WrappedNetworkMonitor monitor = new WrappedNetworkMonitor(context, handler, nai,
-                    defaultRequest);
-            mLastCreatedNetworkMonitor = monitor;
-            return monitor;
-        }
-
-        public WrappedNetworkMonitor getLastCreatedWrappedNetworkMonitor() {
-            return mLastCreatedNetworkMonitor;
-        }
-
-        public void waitForIdle(int timeoutMs) {
-            ((IdleableHandlerThread) mHandlerThread).waitForIdle(timeoutMs);
-        }
-
-        public void waitForIdle() {
-            waitForIdle(TIMEOUT_MS);
-        }
-
-    }
-
-    private interface Criteria {
-        public boolean get();
-    }
-
-    /**
-     * Wait up to 500ms for {@code criteria.get()} to become true, polling.
-     * Fails if 500ms goes by before {@code criteria.get()} to become true.
-     */
-    static private void waitFor(Criteria criteria) {
-        int delays = 0;
-        while (!criteria.get()) {
-            try {
-                Thread.sleep(50);
-            } catch (InterruptedException e) {
-            }
-            if (++delays == 10) fail();
-        }
-    }
-
-    /**
-     * Wait up to TIMEOUT_MS for {@code conditionVariable} to open.
-     * Fails if TIMEOUT_MS goes by before {@code conditionVariable} opens.
-     */
-    static private void waitFor(ConditionVariable conditionVariable) {
-        assertTrue(conditionVariable.block(TIMEOUT_MS));
-    }
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-
-        NetworkMonitor.SetDefaultLingerTime(120);
-
-        // InstrumentationTestRunner prepares a looper, but AndroidJUnitRunner does not.
-        // http://b/25897652 .
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-
-        mServiceContext = new MockContext(getContext());
-        mService = new WrappedConnectivityService(mServiceContext,
-                mock(INetworkManagementService.class),
-                mock(INetworkStatsService.class),
-                mock(INetworkPolicyManager.class));
-
-        mService.systemReady();
-        mCm = new WrappedConnectivityManager(getContext(), mService);
-        mCm.bindProcessToNetwork(null);
-    }
-
-    private int transportToLegacyType(int transport) {
-        switch (transport) {
-            case TRANSPORT_WIFI:
-                return TYPE_WIFI;
-            case TRANSPORT_CELLULAR:
-                return TYPE_MOBILE;
-            default:
-                throw new IllegalStateException("Unknown transport" + transport);
-        }
-    }
-
-    private void verifyActiveNetwork(int transport) {
-        // Test getActiveNetworkInfo()
-        assertNotNull(mCm.getActiveNetworkInfo());
-        assertEquals(transportToLegacyType(transport), mCm.getActiveNetworkInfo().getType());
-        // Test getActiveNetwork()
-        assertNotNull(mCm.getActiveNetwork());
-        assertEquals(mCm.getActiveNetwork(), mCm.getActiveNetworkForUid(Process.myUid()));
-        switch (transport) {
-            case TRANSPORT_WIFI:
-                assertEquals(mCm.getActiveNetwork(), mWiFiNetworkAgent.getNetwork());
-                break;
-            case TRANSPORT_CELLULAR:
-                assertEquals(mCm.getActiveNetwork(), mCellNetworkAgent.getNetwork());
-                break;
-            default:
-                throw new IllegalStateException("Unknown transport" + transport);
-        }
-        // Test getNetworkInfo(Network)
-        assertNotNull(mCm.getNetworkInfo(mCm.getActiveNetwork()));
-        assertEquals(transportToLegacyType(transport), mCm.getNetworkInfo(mCm.getActiveNetwork()).getType());
-        // Test getNetworkCapabilities(Network)
-        assertNotNull(mCm.getNetworkCapabilities(mCm.getActiveNetwork()));
-        assertTrue(mCm.getNetworkCapabilities(mCm.getActiveNetwork()).hasTransport(transport));
-    }
-
-    private void verifyNoNetwork() {
-        // Test getActiveNetworkInfo()
-        assertNull(mCm.getActiveNetworkInfo());
-        // Test getActiveNetwork()
-        assertNull(mCm.getActiveNetwork());
-        assertNull(mCm.getActiveNetworkForUid(Process.myUid()));
-        // Test getAllNetworks()
-        assertEquals(0, mCm.getAllNetworks().length);
-    }
-
-    /**
-     * Return a ConditionVariable that opens when {@code count} numbers of CONNECTIVITY_ACTION
-     * broadcasts are received.
-     */
-    private ConditionVariable waitForConnectivityBroadcasts(final int count) {
-        final ConditionVariable cv = new ConditionVariable();
-        mServiceContext.registerReceiver(new BroadcastReceiver() {
-                    private int remaining = count;
-                    public void onReceive(Context context, Intent intent) {
-                        if (--remaining == 0) {
-                            cv.open();
-                            mServiceContext.unregisterReceiver(this);
-                        }
-                    }
-                }, new IntentFilter(CONNECTIVITY_ACTION));
-        return cv;
-    }
-
-    @LargeTest
-    public void testLingering() throws Exception {
-        verifyNoNetwork();
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        assertNull(mCm.getActiveNetworkInfo());
-        assertNull(mCm.getActiveNetwork());
-        // Test bringing up validated cellular.
-        ConditionVariable cv = waitForConnectivityBroadcasts(1);
-        mCellNetworkAgent.connect(true);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
-        assertEquals(2, mCm.getAllNetworks().length);
-        assertTrue(mCm.getAllNetworks()[0].equals(mCm.getActiveNetwork()) ||
-                mCm.getAllNetworks()[1].equals(mCm.getActiveNetwork()));
-        assertTrue(mCm.getAllNetworks()[0].equals(mWiFiNetworkAgent.getNetwork()) ||
-                mCm.getAllNetworks()[1].equals(mWiFiNetworkAgent.getNetwork()));
-        // Test bringing up validated WiFi.
-        cv = waitForConnectivityBroadcasts(2);
-        mWiFiNetworkAgent.connect(true);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        assertEquals(2, mCm.getAllNetworks().length);
-        assertTrue(mCm.getAllNetworks()[0].equals(mCm.getActiveNetwork()) ||
-                mCm.getAllNetworks()[1].equals(mCm.getActiveNetwork()));
-        assertTrue(mCm.getAllNetworks()[0].equals(mCellNetworkAgent.getNetwork()) ||
-                mCm.getAllNetworks()[1].equals(mCellNetworkAgent.getNetwork()));
-        // Test cellular linger timeout.
-        waitFor(new Criteria() {
-                public boolean get() { return mCm.getAllNetworks().length == 1; } });
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        assertEquals(1, mCm.getAllNetworks().length);
-        assertEquals(mCm.getAllNetworks()[0], mCm.getActiveNetwork());
-        // Test WiFi disconnect.
-        cv = waitForConnectivityBroadcasts(1);
-        mWiFiNetworkAgent.disconnect();
-        waitFor(cv);
-        verifyNoNetwork();
-    }
-
-    @LargeTest
-    public void testValidatedCellularOutscoresUnvalidatedWiFi() throws Exception {
-        // Test bringing up unvalidated WiFi
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        ConditionVariable cv = waitForConnectivityBroadcasts(1);
-        mWiFiNetworkAgent.connect(false);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        // Test bringing up unvalidated cellular
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        mCellNetworkAgent.connect(false);
-        mService.waitForIdle();
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        // Test cellular disconnect.
-        mCellNetworkAgent.disconnect();
-        mService.waitForIdle();
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        // Test bringing up validated cellular
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        cv = waitForConnectivityBroadcasts(2);
-        mCellNetworkAgent.connect(true);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
-        // Test cellular disconnect.
-        cv = waitForConnectivityBroadcasts(2);
-        mCellNetworkAgent.disconnect();
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        // Test WiFi disconnect.
-        cv = waitForConnectivityBroadcasts(1);
-        mWiFiNetworkAgent.disconnect();
-        waitFor(cv);
-        verifyNoNetwork();
-    }
-
-    @LargeTest
-    public void testUnvalidatedWifiOutscoresUnvalidatedCellular() throws Exception {
-        // Test bringing up unvalidated cellular.
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        ConditionVariable cv = waitForConnectivityBroadcasts(1);
-        mCellNetworkAgent.connect(false);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
-        // Test bringing up unvalidated WiFi.
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        cv = waitForConnectivityBroadcasts(2);
-        mWiFiNetworkAgent.connect(false);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        // Test WiFi disconnect.
-        cv = waitForConnectivityBroadcasts(2);
-        mWiFiNetworkAgent.disconnect();
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
-        // Test cellular disconnect.
-        cv = waitForConnectivityBroadcasts(1);
-        mCellNetworkAgent.disconnect();
-        waitFor(cv);
-        verifyNoNetwork();
-    }
-
-    @LargeTest
-    public void testUnlingeringDoesNotValidate() throws Exception {
-        // Test bringing up unvalidated WiFi.
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        ConditionVariable cv = waitForConnectivityBroadcasts(1);
-        mWiFiNetworkAgent.connect(false);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
-                NET_CAPABILITY_VALIDATED));
-        // Test bringing up validated cellular.
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        cv = waitForConnectivityBroadcasts(2);
-        mCellNetworkAgent.connect(true);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
-        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
-                NET_CAPABILITY_VALIDATED));
-        // Test cellular disconnect.
-        cv = waitForConnectivityBroadcasts(2);
-        mCellNetworkAgent.disconnect();
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        // Unlingering a network should not cause it to be marked as validated.
-        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
-                NET_CAPABILITY_VALIDATED));
-    }
-
-    @LargeTest
-    public void testCellularOutscoresWeakWifi() throws Exception {
-        // Test bringing up validated cellular.
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        ConditionVariable cv = waitForConnectivityBroadcasts(1);
-        mCellNetworkAgent.connect(true);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
-        // Test bringing up validated WiFi.
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        cv = waitForConnectivityBroadcasts(2);
-        mWiFiNetworkAgent.connect(true);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        // Test WiFi getting really weak.
-        cv = waitForConnectivityBroadcasts(2);
-        mWiFiNetworkAgent.adjustScore(-11);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
-        // Test WiFi restoring signal strength.
-        cv = waitForConnectivityBroadcasts(2);
-        mWiFiNetworkAgent.adjustScore(11);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        mCellNetworkAgent.disconnect();
-        mWiFiNetworkAgent.disconnect();
-    }
-
-    @LargeTest
-    public void testReapingNetwork() throws Exception {
-        // Test bringing up WiFi without NET_CAPABILITY_INTERNET.
-        // Expect it to be torn down immediately because it satisfies no requests.
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        ConditionVariable cv = mWiFiNetworkAgent.getDisconnectedCV();
-        mWiFiNetworkAgent.connectWithoutInternet();
-        waitFor(cv);
-        // Test bringing up cellular without NET_CAPABILITY_INTERNET.
-        // Expect it to be torn down immediately because it satisfies no requests.
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        cv = mCellNetworkAgent.getDisconnectedCV();
-        mCellNetworkAgent.connectWithoutInternet();
-        waitFor(cv);
-        // Test bringing up validated WiFi.
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        cv = waitForConnectivityBroadcasts(1);
-        mWiFiNetworkAgent.connect(true);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        // Test bringing up unvalidated cellular.
-        // Expect it to be torn down because it could never be the highest scoring network
-        // satisfying the default request even if it validated.
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        cv = mCellNetworkAgent.getDisconnectedCV();
-        mCellNetworkAgent.connect(false);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        cv = mWiFiNetworkAgent.getDisconnectedCV();
-        mWiFiNetworkAgent.disconnect();
-        waitFor(cv);
-    }
-
-    @LargeTest
-    public void testCellularFallback() throws Exception {
-        // Test bringing up validated cellular.
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        ConditionVariable cv = waitForConnectivityBroadcasts(1);
-        mCellNetworkAgent.connect(true);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
-        // Test bringing up validated WiFi.
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        cv = waitForConnectivityBroadcasts(2);
-        mWiFiNetworkAgent.connect(true);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        // Reevaluate WiFi (it'll instantly fail DNS).
-        cv = waitForConnectivityBroadcasts(2);
-        assertTrue(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
-                NET_CAPABILITY_VALIDATED));
-        mCm.reportBadNetwork(mWiFiNetworkAgent.getNetwork());
-        // Should quickly fall back to Cellular.
-        waitFor(cv);
-        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
-                NET_CAPABILITY_VALIDATED));
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
-        // Reevaluate cellular (it'll instantly fail DNS).
-        cv = waitForConnectivityBroadcasts(2);
-        assertTrue(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
-                NET_CAPABILITY_VALIDATED));
-        mCm.reportBadNetwork(mCellNetworkAgent.getNetwork());
-        // Should quickly fall back to WiFi.
-        waitFor(cv);
-        assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
-                NET_CAPABILITY_VALIDATED));
-        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
-                NET_CAPABILITY_VALIDATED));
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        mCellNetworkAgent.disconnect();
-        mWiFiNetworkAgent.disconnect();
-    }
-
-    @LargeTest
-    public void testWiFiFallback() throws Exception {
-        // Test bringing up unvalidated WiFi.
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        ConditionVariable cv = waitForConnectivityBroadcasts(1);
-        mWiFiNetworkAgent.connect(false);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        // Test bringing up validated cellular.
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        cv = waitForConnectivityBroadcasts(2);
-        mCellNetworkAgent.connect(true);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
-        // Reevaluate cellular (it'll instantly fail DNS).
-        cv = waitForConnectivityBroadcasts(2);
-        assertTrue(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
-                NET_CAPABILITY_VALIDATED));
-        mCm.reportBadNetwork(mCellNetworkAgent.getNetwork());
-        // Should quickly fall back to WiFi.
-        waitFor(cv);
-        assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
-                NET_CAPABILITY_VALIDATED));
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        mCellNetworkAgent.disconnect();
-        mWiFiNetworkAgent.disconnect();
-    }
-
-    enum CallbackState {
-        NONE,
-        AVAILABLE,
-        LOSING,
-        LOST
-    }
-
-    /**
-     * Utility NetworkCallback for testing. The caller must explicitly test for all the callbacks
-     * this class receives, by calling expectCallback() exactly once each time a callback is
-     * received. assertNoCallback may be called at any time.
-     */
-    private class TestNetworkCallback extends NetworkCallback {
-        private final ConditionVariable mConditionVariable = new ConditionVariable();
-        private CallbackState mLastCallback = CallbackState.NONE;
-        private Network mLastNetwork;
-
-        public void onAvailable(Network network) {
-            assertEquals(CallbackState.NONE, mLastCallback);
-            mLastCallback = CallbackState.AVAILABLE;
-            mLastNetwork = network;
-            mConditionVariable.open();
-        }
-
-        public void onLosing(Network network, int maxMsToLive) {
-            assertEquals(CallbackState.NONE, mLastCallback);
-            mLastCallback = CallbackState.LOSING;
-            mLastNetwork = network;
-            mConditionVariable.open();
-        }
-
-        public void onLost(Network network) {
-            assertEquals(CallbackState.NONE, mLastCallback);
-            mLastCallback = CallbackState.LOST;
-            mLastNetwork = network;
-            mConditionVariable.open();
-        }
-
-        void expectCallback(CallbackState state) {
-            expectCallback(state, null);
-        }
-
-        void expectCallback(CallbackState state, MockNetworkAgent mockAgent) {
-            waitFor(mConditionVariable);
-            assertEquals(state, mLastCallback);
-            if (mockAgent != null) {
-                assertEquals(mockAgent.getNetwork(), mLastNetwork);
-            }
-            mLastCallback = CallbackState.NONE;
-            mLastNetwork = null;
-            mConditionVariable.close();
-        }
-
-        void assertNoCallback() {
-            assertEquals(CallbackState.NONE, mLastCallback);
-        }
-    }
-
-    @LargeTest
-    public void testStateChangeNetworkCallbacks() throws Exception {
-        final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback();
-        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
-        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
-                .addTransportType(TRANSPORT_WIFI).build();
-        final NetworkRequest cellRequest = new NetworkRequest.Builder()
-                .addTransportType(TRANSPORT_CELLULAR).build();
-        mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
-        mCm.registerNetworkCallback(cellRequest, cellNetworkCallback);
-
-        // Test unvalidated networks
-        ConditionVariable cv = waitForConnectivityBroadcasts(1);
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        mCellNetworkAgent.connect(false);
-        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE);
-        wifiNetworkCallback.assertNoCallback();
-        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
-        waitFor(cv);
-
-        // This should not trigger spurious onAvailable() callbacks, b/21762680.
-        mCellNetworkAgent.adjustScore(-1);
-        mService.waitForIdle();
-        wifiNetworkCallback.assertNoCallback();
-        cellNetworkCallback.assertNoCallback();
-        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
-
-        cv = waitForConnectivityBroadcasts(2);
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        mWiFiNetworkAgent.connect(false);
-        wifiNetworkCallback.expectCallback(CallbackState.AVAILABLE);
-        cellNetworkCallback.assertNoCallback();
-        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
-        waitFor(cv);
-
-        cv = waitForConnectivityBroadcasts(2);
-        mWiFiNetworkAgent.disconnect();
-        wifiNetworkCallback.expectCallback(CallbackState.LOST);
-        cellNetworkCallback.assertNoCallback();
-        waitFor(cv);
-
-        cv = waitForConnectivityBroadcasts(1);
-        mCellNetworkAgent.disconnect();
-        cellNetworkCallback.expectCallback(CallbackState.LOST);
-        wifiNetworkCallback.assertNoCallback();
-        waitFor(cv);
-
-        // Test validated networks
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        mCellNetworkAgent.connect(true);
-        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE);
-        wifiNetworkCallback.assertNoCallback();
-        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
-
-        // This should not trigger spurious onAvailable() callbacks, b/21762680.
-        mCellNetworkAgent.adjustScore(-1);
-        mService.waitForIdle();
-        wifiNetworkCallback.assertNoCallback();
-        cellNetworkCallback.assertNoCallback();
-        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
-
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        mWiFiNetworkAgent.connect(true);
-        wifiNetworkCallback.expectCallback(CallbackState.AVAILABLE);
-        cellNetworkCallback.expectCallback(CallbackState.LOSING);
-        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
-
-        mWiFiNetworkAgent.disconnect();
-        wifiNetworkCallback.expectCallback(CallbackState.LOST);
-        cellNetworkCallback.assertNoCallback();
-
-        mCellNetworkAgent.disconnect();
-        cellNetworkCallback.expectCallback(CallbackState.LOST);
-        wifiNetworkCallback.assertNoCallback();
-    }
-
-    private void tryNetworkFactoryRequests(int capability) throws Exception {
-        // Verify NOT_RESTRICTED is set appropriately
-        final NetworkCapabilities nc = new NetworkRequest.Builder().addCapability(capability)
-                .build().networkCapabilities;
-        if (capability == NET_CAPABILITY_CBS || capability == NET_CAPABILITY_DUN ||
-                capability == NET_CAPABILITY_EIMS || capability == NET_CAPABILITY_FOTA ||
-                capability == NET_CAPABILITY_IA || capability == NET_CAPABILITY_IMS ||
-                capability == NET_CAPABILITY_RCS || capability == NET_CAPABILITY_XCAP) {
-            assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
-        } else {
-            assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
-        }
-
-        NetworkCapabilities filter = new NetworkCapabilities();
-        filter.addCapability(capability);
-        final HandlerThread handlerThread = new HandlerThread("testNetworkFactoryRequests");
-        handlerThread.start();
-        final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
-                mServiceContext, "testFactory", filter);
-        testFactory.setScoreFilter(40);
-        ConditionVariable cv = testFactory.getNetworkStartedCV();
-        testFactory.expectAddRequests(1);
-        testFactory.register();
-        testFactory.waitForNetworkRequests(1);
-        int expectedRequestCount = 1;
-        NetworkCallback networkCallback = null;
-        // For non-INTERNET capabilities we cannot rely on the default request being present, so
-        // add one.
-        if (capability != NET_CAPABILITY_INTERNET) {
-            assertFalse(testFactory.getMyStartRequested());
-            NetworkRequest request = new NetworkRequest.Builder().addCapability(capability).build();
-            networkCallback = new NetworkCallback();
-            testFactory.expectAddRequests(1);
-            mCm.requestNetwork(request, networkCallback);
-            expectedRequestCount++;
-            testFactory.waitForNetworkRequests(expectedRequestCount);
-        }
-        waitFor(cv);
-        assertEquals(expectedRequestCount, testFactory.getMyRequestCount());
-        assertTrue(testFactory.getMyStartRequested());
-
-        // Now bring in a higher scored network.
-        MockNetworkAgent testAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        // Rather than create a validated network which complicates things by registering it's
-        // own NetworkRequest during startup, just bump up the score to cancel out the
-        // unvalidated penalty.
-        testAgent.adjustScore(40);
-        cv = testFactory.getNetworkStoppedCV();
-
-        // When testAgent connects, ConnectivityService will re-send us all current requests with
-        // the new score. There are expectedRequestCount such requests, and we must wait for all of
-        // them.
-        testFactory.expectAddRequests(expectedRequestCount);
-        testAgent.connect(false);
-        testAgent.addCapability(capability);
-        waitFor(cv);
-        testFactory.waitForNetworkRequests(expectedRequestCount);
-        assertFalse(testFactory.getMyStartRequested());
-
-        // Bring in a bunch of requests.
-        testFactory.expectAddRequests(10);
-        assertEquals(expectedRequestCount, testFactory.getMyRequestCount());
-        ConnectivityManager.NetworkCallback[] networkCallbacks =
-                new ConnectivityManager.NetworkCallback[10];
-        for (int i = 0; i< networkCallbacks.length; i++) {
-            networkCallbacks[i] = new ConnectivityManager.NetworkCallback();
-            NetworkRequest.Builder builder = new NetworkRequest.Builder();
-            builder.addCapability(capability);
-            mCm.requestNetwork(builder.build(), networkCallbacks[i]);
-        }
-        testFactory.waitForNetworkRequests(10 + expectedRequestCount);
-        assertFalse(testFactory.getMyStartRequested());
-
-        // Remove the requests.
-        testFactory.expectRemoveRequests(10);
-        for (int i = 0; i < networkCallbacks.length; i++) {
-            mCm.unregisterNetworkCallback(networkCallbacks[i]);
-        }
-        testFactory.waitForNetworkRequests(expectedRequestCount);
-        assertFalse(testFactory.getMyStartRequested());
-
-        // Drop the higher scored network.
-        cv = testFactory.getNetworkStartedCV();
-        testAgent.disconnect();
-        waitFor(cv);
-        assertEquals(expectedRequestCount, testFactory.getMyRequestCount());
-        assertTrue(testFactory.getMyStartRequested());
-
-        testFactory.unregister();
-        if (networkCallback != null) mCm.unregisterNetworkCallback(networkCallback);
-        handlerThread.quit();
-    }
-
-    @LargeTest
-    public void testNetworkFactoryRequests() throws Exception {
-        tryNetworkFactoryRequests(NET_CAPABILITY_MMS);
-        tryNetworkFactoryRequests(NET_CAPABILITY_SUPL);
-        tryNetworkFactoryRequests(NET_CAPABILITY_DUN);
-        tryNetworkFactoryRequests(NET_CAPABILITY_FOTA);
-        tryNetworkFactoryRequests(NET_CAPABILITY_IMS);
-        tryNetworkFactoryRequests(NET_CAPABILITY_CBS);
-        tryNetworkFactoryRequests(NET_CAPABILITY_WIFI_P2P);
-        tryNetworkFactoryRequests(NET_CAPABILITY_IA);
-        tryNetworkFactoryRequests(NET_CAPABILITY_RCS);
-        tryNetworkFactoryRequests(NET_CAPABILITY_XCAP);
-        tryNetworkFactoryRequests(NET_CAPABILITY_EIMS);
-        tryNetworkFactoryRequests(NET_CAPABILITY_NOT_METERED);
-        tryNetworkFactoryRequests(NET_CAPABILITY_INTERNET);
-        tryNetworkFactoryRequests(NET_CAPABILITY_TRUSTED);
-        tryNetworkFactoryRequests(NET_CAPABILITY_NOT_VPN);
-        // Skipping VALIDATED and CAPTIVE_PORTAL as they're disallowed.
-    }
-
-    @LargeTest
-    public void testNoMutableNetworkRequests() throws Exception {
-        PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, new Intent("a"), 0);
-        NetworkRequest.Builder builder = new NetworkRequest.Builder();
-        builder.addCapability(NET_CAPABILITY_VALIDATED);
-        try {
-            mCm.requestNetwork(builder.build(), new NetworkCallback());
-            fail();
-        } catch (IllegalArgumentException expected) {}
-        try {
-            mCm.requestNetwork(builder.build(), pendingIntent);
-            fail();
-        } catch (IllegalArgumentException expected) {}
-        builder = new NetworkRequest.Builder();
-        builder.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
-        try {
-            mCm.requestNetwork(builder.build(), new NetworkCallback());
-            fail();
-        } catch (IllegalArgumentException expected) {}
-        try {
-            mCm.requestNetwork(builder.build(), pendingIntent);
-            fail();
-        } catch (IllegalArgumentException expected) {}
-    }
-
-    @LargeTest
-    public void testMMSonWiFi() throws Exception {
-        // Test bringing up cellular without MMS NetworkRequest gets reaped
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        mCellNetworkAgent.addCapability(NET_CAPABILITY_MMS);
-        ConditionVariable cv = mCellNetworkAgent.getDisconnectedCV();
-        mCellNetworkAgent.connectWithoutInternet();
-        waitFor(cv);
-        waitFor(new Criteria() {
-                public boolean get() { return mCm.getAllNetworks().length == 0; } });
-        verifyNoNetwork();
-        // Test bringing up validated WiFi.
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        cv = waitForConnectivityBroadcasts(1);
-        mWiFiNetworkAgent.connect(true);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        // Register MMS NetworkRequest
-        NetworkRequest.Builder builder = new NetworkRequest.Builder();
-        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS);
-        final TestNetworkCallback networkCallback = new TestNetworkCallback();
-        mCm.requestNetwork(builder.build(), networkCallback);
-        // Test bringing up unvalidated cellular with MMS
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        mCellNetworkAgent.addCapability(NET_CAPABILITY_MMS);
-        mCellNetworkAgent.connectWithoutInternet();
-        networkCallback.expectCallback(CallbackState.AVAILABLE);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        // Test releasing NetworkRequest disconnects cellular with MMS
-        cv = mCellNetworkAgent.getDisconnectedCV();
-        mCm.unregisterNetworkCallback(networkCallback);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-    }
-
-    @LargeTest
-    public void testMMSonCell() throws Exception {
-        // Test bringing up cellular without MMS
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        ConditionVariable cv = waitForConnectivityBroadcasts(1);
-        mCellNetworkAgent.connect(false);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
-        // Register MMS NetworkRequest
-        NetworkRequest.Builder builder = new NetworkRequest.Builder();
-        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS);
-        final TestNetworkCallback networkCallback = new TestNetworkCallback();
-        mCm.requestNetwork(builder.build(), networkCallback);
-        // Test bringing up MMS cellular network
-        MockNetworkAgent mmsNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        mmsNetworkAgent.addCapability(NET_CAPABILITY_MMS);
-        mmsNetworkAgent.connectWithoutInternet();
-        networkCallback.expectCallback(CallbackState.AVAILABLE);
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
-        // Test releasing MMS NetworkRequest does not disconnect main cellular NetworkAgent
-        cv = mmsNetworkAgent.getDisconnectedCV();
-        mCm.unregisterNetworkCallback(networkCallback);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
-    }
-
-    @LargeTest
-    public void testCaptivePortal() {
-        final TestNetworkCallback captivePortalCallback = new TestNetworkCallback();
-        final NetworkRequest captivePortalRequest = new NetworkRequest.Builder()
-                .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build();
-        mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback);
-
-        final TestNetworkCallback validatedCallback = new TestNetworkCallback();
-        final NetworkRequest validatedRequest = new NetworkRequest.Builder()
-                .addCapability(NET_CAPABILITY_VALIDATED).build();
-        mCm.registerNetworkCallback(validatedRequest, validatedCallback);
-
-        // Bring up a network with a captive portal.
-        // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL.
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        String firstRedirectUrl = "http://example.com/firstPath";
-        mWiFiNetworkAgent.connectWithCaptivePortal(firstRedirectUrl);
-        captivePortalCallback.expectCallback(CallbackState.AVAILABLE);
-        assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), firstRedirectUrl);
-
-        // Take down network.
-        // Expect onLost callback.
-        mWiFiNetworkAgent.disconnect();
-        captivePortalCallback.expectCallback(CallbackState.LOST);
-
-        // Bring up a network with a captive portal.
-        // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL.
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        String secondRedirectUrl = "http://example.com/secondPath";
-        mWiFiNetworkAgent.connectWithCaptivePortal(secondRedirectUrl);
-        captivePortalCallback.expectCallback(CallbackState.AVAILABLE);
-        assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), secondRedirectUrl);
-
-        // Make captive portal disappear then revalidate.
-        // Expect onLost callback because network no longer provides NET_CAPABILITY_CAPTIVE_PORTAL.
-        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 204;
-        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true);
-        captivePortalCallback.expectCallback(CallbackState.LOST);
-
-        // Expect NET_CAPABILITY_VALIDATED onAvailable callback.
-        validatedCallback.expectCallback(CallbackState.AVAILABLE);
-
-        // Break network connectivity.
-        // Expect NET_CAPABILITY_VALIDATED onLost callback.
-        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 500;
-        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), false);
-        validatedCallback.expectCallback(CallbackState.LOST);
-    }
-
-    @SmallTest
-    public void testInvalidNetworkSpecifier() {
-        boolean execptionCalled = true;
-
-        try {
-            NetworkRequest.Builder builder = new NetworkRequest.Builder();
-            builder.setNetworkSpecifier(MATCH_ALL_REQUESTS_NETWORK_SPECIFIER);
-            execptionCalled = false;
-        } catch (IllegalArgumentException e) {
-            // do nothing - should get here
-        }
-
-        assertTrue("NetworkReqeuest builder with MATCH_ALL_REQUESTS_NETWORK_SPECIFIER",
-                execptionCalled);
-
-        try {
-            NetworkCapabilities networkCapabilities = new NetworkCapabilities();
-            networkCapabilities.addTransportType(TRANSPORT_WIFI)
-                    .setNetworkSpecifier(NetworkCapabilities.MATCH_ALL_REQUESTS_NETWORK_SPECIFIER);
-            mService.requestNetwork(networkCapabilities, null, 0, null,
-                    ConnectivityManager.TYPE_WIFI);
-            execptionCalled = false;
-        } catch (IllegalArgumentException e) {
-            // do nothing - should get here
-        }
-
-        assertTrue("ConnectivityService requestNetwork with MATCH_ALL_REQUESTS_NETWORK_SPECIFIER",
-                execptionCalled);
-    }
-
-    @LargeTest
-    public void testRegisterDefaultNetworkCallback() throws Exception {
-        final TestNetworkCallback defaultNetworkCallback = new TestNetworkCallback();
-        mCm.registerDefaultNetworkCallback(defaultNetworkCallback);
-        defaultNetworkCallback.assertNoCallback();
-
-        // Create a TRANSPORT_CELLULAR request to keep the mobile interface up
-        // whenever Wi-Fi is up. Without this, the mobile network agent is
-        // reaped before any other activity can take place.
-        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
-        final NetworkRequest cellRequest = new NetworkRequest.Builder()
-                .addTransportType(TRANSPORT_CELLULAR).build();
-        mCm.requestNetwork(cellRequest, cellNetworkCallback);
-        cellNetworkCallback.assertNoCallback();
-
-        // Bring up cell and expect CALLBACK_AVAILABLE.
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        mCellNetworkAgent.connect(true);
-        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
-        defaultNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
-
-        // Bring up wifi and expect CALLBACK_AVAILABLE.
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        mWiFiNetworkAgent.connect(true);
-        cellNetworkCallback.assertNoCallback();
-        defaultNetworkCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
-
-        // Bring down cell. Expect no default network callback, since it wasn't the default.
-        mCellNetworkAgent.disconnect();
-        cellNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
-        defaultNetworkCallback.assertNoCallback();
-
-        // Bring up cell. Expect no default network callback, since it won't be the default.
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        mCellNetworkAgent.connect(true);
-        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
-        defaultNetworkCallback.assertNoCallback();
-
-        // Bring down wifi. Expect the default network callback to notified of LOST wifi
-        // followed by AVAILABLE cell.
-        mWiFiNetworkAgent.disconnect();
-        cellNetworkCallback.assertNoCallback();
-        defaultNetworkCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
-        defaultNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
-        mCellNetworkAgent.disconnect();
-        cellNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
-        defaultNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
-    }
-
-    private static class TestKeepaliveCallback extends PacketKeepaliveCallback {
-
-        public static enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR };
-
-        private class CallbackValue {
-            public CallbackType callbackType;
-            public int error;
-
-            public CallbackValue(CallbackType type) {
-                this.callbackType = type;
-                this.error = PacketKeepalive.SUCCESS;
-                assertTrue("onError callback must have error", type != CallbackType.ON_ERROR);
-            }
-
-            public CallbackValue(CallbackType type, int error) {
-                this.callbackType = type;
-                this.error = error;
-                assertEquals("error can only be set for onError", type, CallbackType.ON_ERROR);
-            }
-
-            @Override
-            public boolean equals(Object o) {
-                return o instanceof CallbackValue &&
-                        this.callbackType == ((CallbackValue) o).callbackType &&
-                        this.error == ((CallbackValue) o).error;
-            }
-
-            @Override
-            public String toString() {
-                return String.format("%s(%s, %d)", getClass().getSimpleName(), callbackType, error);
-            }
-        }
-
-        private LinkedBlockingQueue<CallbackValue> mCallbacks = new LinkedBlockingQueue<>();
-
-        @Override
-        public void onStarted() {
-            mCallbacks.add(new CallbackValue(CallbackType.ON_STARTED));
-        }
-
-        @Override
-        public void onStopped() {
-            mCallbacks.add(new CallbackValue(CallbackType.ON_STOPPED));
-        }
-
-        @Override
-        public void onError(int error) {
-            mCallbacks.add(new CallbackValue(CallbackType.ON_ERROR, error));
-        }
-
-        private void expectCallback(CallbackValue callbackValue) {
-            try {
-                assertEquals(
-                        callbackValue,
-                        mCallbacks.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-            } catch (InterruptedException e) {
-                fail(callbackValue.callbackType + " callback not seen after " + TIMEOUT_MS + " ms");
-            }
-        }
-
-        public void expectStarted() {
-            expectCallback(new CallbackValue(CallbackType.ON_STARTED));
-        }
-
-        public void expectStopped() {
-            expectCallback(new CallbackValue(CallbackType.ON_STOPPED));
-        }
-
-        public void expectError(int error) {
-            expectCallback(new CallbackValue(CallbackType.ON_ERROR, error));
-        }
-    }
-
-    private Network connectKeepaliveNetwork(LinkProperties lp) {
-        // Ensure the network is disconnected before we do anything.
-        if (mWiFiNetworkAgent != null) {
-            assertNull(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()));
-        }
-
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        ConditionVariable cv = waitForConnectivityBroadcasts(1);
-        mWiFiNetworkAgent.connect(true);
-        waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        mWiFiNetworkAgent.sendLinkProperties(lp);
-        mService.waitForIdle();
-        return mWiFiNetworkAgent.getNetwork();
-    }
-
-    public void testPacketKeepalives() throws Exception {
-        InetAddress myIPv4 = InetAddress.getByName("192.0.2.129");
-        InetAddress notMyIPv4 = InetAddress.getByName("192.0.2.35");
-        InetAddress myIPv6 = InetAddress.getByName("2001:db8::1");
-        InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8");
-        InetAddress dstIPv6 = InetAddress.getByName("2001:4860:4860::8888");
-
-        LinkProperties lp = new LinkProperties();
-        lp.setInterfaceName("wlan12");
-        lp.addLinkAddress(new LinkAddress(myIPv6, 64));
-        lp.addLinkAddress(new LinkAddress(myIPv4, 25));
-        lp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234")));
-        lp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254")));
-
-        Network notMyNet = new Network(61234);
-        Network myNet = connectKeepaliveNetwork(lp);
-
-        TestKeepaliveCallback callback = new TestKeepaliveCallback();
-        PacketKeepalive ka;
-
-        // Attempt to start keepalives with invalid parameters and check for errors.
-        ka = mCm.startNattKeepalive(notMyNet, 25, callback, myIPv4, 1234, dstIPv4);
-        callback.expectError(PacketKeepalive.ERROR_INVALID_NETWORK);
-
-        ka = mCm.startNattKeepalive(myNet, 19, callback, notMyIPv4, 1234, dstIPv4);
-        callback.expectError(PacketKeepalive.ERROR_INVALID_INTERVAL);
-
-        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 1234, dstIPv6);
-        callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
-
-        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv6, 1234, dstIPv4);
-        callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
-
-        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv6, 1234, dstIPv6);
-        callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS);  // NAT-T is IPv4-only.
-
-        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 123456, dstIPv4);
-        callback.expectError(PacketKeepalive.ERROR_INVALID_PORT);
-
-        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 123456, dstIPv4);
-        callback.expectError(PacketKeepalive.ERROR_INVALID_PORT);
-
-        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 12345, dstIPv4);
-        callback.expectError(PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED);
-
-        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 12345, dstIPv4);
-        callback.expectError(PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED);
-
-        // Check that a started keepalive can be stopped.
-        mWiFiNetworkAgent.setStartKeepaliveError(PacketKeepalive.SUCCESS);
-        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 12345, dstIPv4);
-        callback.expectStarted();
-        mWiFiNetworkAgent.setStopKeepaliveError(PacketKeepalive.SUCCESS);
-        ka.stop();
-        callback.expectStopped();
-
-        // Check that deleting the IP address stops the keepalive.
-        LinkProperties bogusLp = new LinkProperties(lp);
-        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 12345, dstIPv4);
-        callback.expectStarted();
-        bogusLp.removeLinkAddress(new LinkAddress(myIPv4, 25));
-        bogusLp.addLinkAddress(new LinkAddress(notMyIPv4, 25));
-        mWiFiNetworkAgent.sendLinkProperties(bogusLp);
-        callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
-        mWiFiNetworkAgent.sendLinkProperties(lp);
-
-        // Check that a started keepalive is stopped correctly when the network disconnects.
-        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 12345, dstIPv4);
-        callback.expectStarted();
-        mWiFiNetworkAgent.disconnect();
-        callback.expectError(PacketKeepalive.ERROR_INVALID_NETWORK);
-
-        // ... and that stopping it after that has no adverse effects.
-        assertNull(mCm.getNetworkCapabilities(myNet));
-        ka.stop();
-
-        // Reconnect.
-        myNet = connectKeepaliveNetwork(lp);
-        mWiFiNetworkAgent.setStartKeepaliveError(PacketKeepalive.SUCCESS);
-
-        // Check things work as expected when the keepalive is stopped and the network disconnects.
-        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 12345, dstIPv4);
-        callback.expectStarted();
-        ka.stop();
-        mWiFiNetworkAgent.disconnect();
-        mService.waitForIdle();
-        callback.expectStopped();
-
-        // Reconnect.
-        myNet = connectKeepaliveNetwork(lp);
-        mWiFiNetworkAgent.setStartKeepaliveError(PacketKeepalive.SUCCESS);
-
-        // Check that keepalive slots start from 1 and increment. The first one gets slot 1.
-        mWiFiNetworkAgent.setExpectedKeepaliveSlot(1);
-        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 12345, dstIPv4);
-        callback.expectStarted();
-
-        // The second one gets slot 2.
-        mWiFiNetworkAgent.setExpectedKeepaliveSlot(2);
-        TestKeepaliveCallback callback2 = new TestKeepaliveCallback();
-        PacketKeepalive ka2 = mCm.startNattKeepalive(myNet, 25, callback2, myIPv4, 6789, dstIPv4);
-        callback2.expectStarted();
-
-        // Now stop the first one and create a third. This also gets slot 1.
-        ka.stop();
-        callback.expectStopped();
-
-        mWiFiNetworkAgent.setExpectedKeepaliveSlot(1);
-        TestKeepaliveCallback callback3 = new TestKeepaliveCallback();
-        PacketKeepalive ka3 = mCm.startNattKeepalive(myNet, 25, callback3, myIPv4, 9876, dstIPv4);
-        callback3.expectStarted();
-
-        ka2.stop();
-        callback2.expectStopped();
-
-        ka3.stop();
-        callback3.expectStopped();
-    }
-
-    @SmallTest
-    public void testGetCaptivePortalServerUrl() throws Exception {
-        String url = mCm.getCaptivePortalServerUrl();
-        assertEquals("http://connectivitycheck.gstatic.com/generate_204", url);
-    }
-
-    private static class TestNetworkPinner extends NetworkPinner {
-        public static boolean awaitPin(int timeoutMs) {
-            synchronized(sLock) {
-                if (sNetwork == null) {
-                    try {
-                        sLock.wait(timeoutMs);
-                    } catch (InterruptedException e) {}
-                }
-                return sNetwork != null;
-            }
-        }
-
-        public static boolean awaitUnpin(int timeoutMs) {
-            synchronized(sLock) {
-                if (sNetwork != null) {
-                    try {
-                        sLock.wait(timeoutMs);
-                    } catch (InterruptedException e) {}
-                }
-                return sNetwork == null;
-            }
-        }
-    }
-
-    private void assertPinnedToWifiWithCellDefault() {
-        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getBoundNetworkForProcess());
-        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
-    }
-
-    private void assertPinnedToWifiWithWifiDefault() {
-        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getBoundNetworkForProcess());
-        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
-    }
-
-    private void assertNotPinnedToWifi() {
-        assertNull(mCm.getBoundNetworkForProcess());
-        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
-    }
-
-    @SmallTest
-    public void testNetworkPinner() {
-        NetworkRequest wifiRequest = new NetworkRequest.Builder()
-                .addTransportType(TRANSPORT_WIFI)
-                .build();
-        assertNull(mCm.getBoundNetworkForProcess());
-
-        TestNetworkPinner.pin(mServiceContext, wifiRequest);
-        assertNull(mCm.getBoundNetworkForProcess());
-
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
-        mCellNetworkAgent.connect(true);
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        mWiFiNetworkAgent.connect(false);
-
-        // When wi-fi connects, expect to be pinned.
-        assertTrue(TestNetworkPinner.awaitPin(100));
-        assertPinnedToWifiWithCellDefault();
-
-        // Disconnect and expect the pin to drop.
-        mWiFiNetworkAgent.disconnect();
-        assertTrue(TestNetworkPinner.awaitUnpin(100));
-        assertNotPinnedToWifi();
-
-        // Reconnecting does not cause the pin to come back.
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        mWiFiNetworkAgent.connect(false);
-        assertFalse(TestNetworkPinner.awaitPin(100));
-        assertNotPinnedToWifi();
-
-        // Pinning while connected causes the pin to take effect immediately.
-        TestNetworkPinner.pin(mServiceContext, wifiRequest);
-        assertTrue(TestNetworkPinner.awaitPin(100));
-        assertPinnedToWifiWithCellDefault();
-
-        // Explicitly unpin and expect to use the default network again.
-        TestNetworkPinner.unpin();
-        assertNotPinnedToWifi();
-
-        // Disconnect cell and wifi.
-        ConditionVariable cv = waitForConnectivityBroadcasts(3);  // cell down, wifi up, wifi down.
-        mCellNetworkAgent.disconnect();
-        mWiFiNetworkAgent.disconnect();
-        waitFor(cv);
-
-        // Pinning takes effect even if the pinned network is the default when the pin is set...
-        TestNetworkPinner.pin(mServiceContext, wifiRequest);
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        mWiFiNetworkAgent.connect(false);
-        assertTrue(TestNetworkPinner.awaitPin(100));
-        assertPinnedToWifiWithWifiDefault();
-
-        // ... and is maintained even when that network is no longer the default.
-        cv = waitForConnectivityBroadcasts(1);
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        mCellNetworkAgent.connect(true);
-        waitFor(cv);
-        assertPinnedToWifiWithCellDefault();
-    }
-
-    @SmallTest
-    public void testNetworkRequestMaximum() {
-        final int MAX_REQUESTS = 100;
-        // Test that the limit is enforced when MAX_REQUESTS simultaneous requests are added.
-        NetworkRequest networkRequest = new NetworkRequest.Builder().build();
-        ArrayList<NetworkCallback> networkCallbacks = new ArrayList<NetworkCallback>();
-        try {
-            for (int i = 0; i < MAX_REQUESTS; i++) {
-                NetworkCallback networkCallback = new NetworkCallback();
-                mCm.requestNetwork(networkRequest, networkCallback);
-                networkCallbacks.add(networkCallback);
-            }
-            fail("Registering " + MAX_REQUESTS + " NetworkRequests did not throw exception");
-        } catch (IllegalArgumentException expected) {}
-        for (NetworkCallback networkCallback : networkCallbacks) {
-            mCm.unregisterNetworkCallback(networkCallback);
-        }
-        networkCallbacks.clear();
-
-        try {
-            for (int i = 0; i < MAX_REQUESTS; i++) {
-                NetworkCallback networkCallback = new NetworkCallback();
-                mCm.registerNetworkCallback(networkRequest, networkCallback);
-                networkCallbacks.add(networkCallback);
-            }
-            fail("Registering " + MAX_REQUESTS + " NetworkCallbacks did not throw exception");
-        } catch (IllegalArgumentException expected) {}
-        for (NetworkCallback networkCallback : networkCallbacks) {
-            mCm.unregisterNetworkCallback(networkCallback);
-        }
-        networkCallbacks.clear();
-
-        ArrayList<PendingIntent> pendingIntents = new ArrayList<PendingIntent>();
-        try {
-            for (int i = 0; i < MAX_REQUESTS + 1; i++) {
-                PendingIntent pendingIntent =
-                        PendingIntent.getBroadcast(mContext, 0, new Intent("a" + i), 0);
-                mCm.requestNetwork(networkRequest, pendingIntent);
-                pendingIntents.add(pendingIntent);
-            }
-            fail("Registering " + MAX_REQUESTS +
-                    " PendingIntent NetworkRequests did not throw exception");
-        } catch (IllegalArgumentException expected) {}
-        for (PendingIntent pendingIntent : pendingIntents) {
-            mCm.unregisterNetworkCallback(pendingIntent);
-        }
-        pendingIntents.clear();
-
-        try {
-            for (int i = 0; i < MAX_REQUESTS + 1; i++) {
-                PendingIntent pendingIntent =
-                        PendingIntent.getBroadcast(mContext, 0, new Intent("a" + i), 0);
-                mCm.registerNetworkCallback(networkRequest, pendingIntent);
-                pendingIntents.add(pendingIntent);
-            }
-            fail("Registering " + MAX_REQUESTS +
-                    " PendingIntent NetworkCallbacks did not throw exception");
-        } catch (IllegalArgumentException expected) {}
-        for (PendingIntent pendingIntent : pendingIntents) {
-            mCm.unregisterNetworkCallback(pendingIntent);
-        }
-        pendingIntents.clear();
-        mService.waitForIdle(5000);
-
-        // Test that the limit is not hit when MAX_REQUESTS requests are added and removed.
-        for (int i = 0; i < MAX_REQUESTS; i++) {
-            NetworkCallback networkCallback = new NetworkCallback();
-            mCm.requestNetwork(networkRequest, networkCallback);
-            mCm.unregisterNetworkCallback(networkCallback);
-        }
-        mService.waitForIdle();
-        for (int i = 0; i < MAX_REQUESTS; i++) {
-            NetworkCallback networkCallback = new NetworkCallback();
-            mCm.registerNetworkCallback(networkRequest, networkCallback);
-            mCm.unregisterNetworkCallback(networkCallback);
-        }
-        mService.waitForIdle();
-        for (int i = 0; i < MAX_REQUESTS; i++) {
-            PendingIntent pendingIntent =
-                    PendingIntent.getBroadcast(mContext, 0, new Intent("b" + i), 0);
-            mCm.requestNetwork(networkRequest, pendingIntent);
-            mCm.unregisterNetworkCallback(pendingIntent);
-        }
-        mService.waitForIdle();
-        for (int i = 0; i < MAX_REQUESTS; i++) {
-            PendingIntent pendingIntent =
-                    PendingIntent.getBroadcast(mContext, 0, new Intent("c" + i), 0);
-            mCm.registerNetworkCallback(networkRequest, pendingIntent);
-            mCm.unregisterNetworkCallback(pendingIntent);
-        }
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java
index 0d5daa5..f841bf9 100644
--- a/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java
@@ -24,6 +24,7 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
 import com.android.server.net.BaseNetworkObserver;
+import com.android.internal.util.test.BroadcastInterceptingContext;
 
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkStatsObserversTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkStatsObserversTest.java
index 21560ac..5eee7b9 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkStatsObserversTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkStatsObserversTest.java
@@ -25,6 +25,7 @@
 import static org.mockito.Mockito.when;
 
 import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.METERED_NO;
 import static android.net.NetworkStats.ROAMING_NO;
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkTemplate.buildTemplateMobileAll;
@@ -336,7 +337,7 @@
         // Baseline
         NetworkStats xtSnapshot = null;
         NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */)
-                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                         BASE_BYTES, 2L, BASE_BYTES, 2L, 0L);
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces,
@@ -344,7 +345,7 @@
 
         // Delta
         uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */)
-                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                         BASE_BYTES + THRESHOLD_BYTES, 2L, BASE_BYTES + THRESHOLD_BYTES, 2L, 0L);
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces,
@@ -374,7 +375,7 @@
         // Baseline
         NetworkStats xtSnapshot = null;
         NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */)
-                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                         BASE_BYTES, 2L, BASE_BYTES, 2L, 0L);
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces,
@@ -382,7 +383,7 @@
 
         // Delta
         uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */)
-                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                         BASE_BYTES + THRESHOLD_BYTES, 2L, BASE_BYTES + THRESHOLD_BYTES, 2L, 0L);
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces,
@@ -412,7 +413,7 @@
         // Baseline
         NetworkStats xtSnapshot = null;
         NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */)
-                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                         BASE_BYTES, 2L, BASE_BYTES, 2L, 0L);
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces,
@@ -420,7 +421,7 @@
 
         // Delta
         uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */)
-                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                         BASE_BYTES + THRESHOLD_BYTES, 2L, BASE_BYTES + THRESHOLD_BYTES, 2L, 0L);
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces,
@@ -450,16 +451,17 @@
         // Baseline
         NetworkStats xtSnapshot = null;
         NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */)
-                .addValues(TEST_IFACE, UID_ANOTHER_USER, SET_DEFAULT, TAG_NONE, ROAMING_NO,
-                        BASE_BYTES, 2L, BASE_BYTES, 2L, 0L);
+                .addValues(TEST_IFACE, UID_ANOTHER_USER, SET_DEFAULT, TAG_NONE, METERED_NO,
+                        ROAMING_NO, BASE_BYTES, 2L, BASE_BYTES, 2L, 0L);
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces,
                 VPN_INFO, TEST_START);
 
         // Delta
         uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */)
-                .addValues(TEST_IFACE, UID_ANOTHER_USER, SET_DEFAULT, TAG_NONE, ROAMING_NO,
-                        BASE_BYTES + THRESHOLD_BYTES, 2L, BASE_BYTES + THRESHOLD_BYTES, 2L, 0L);
+                .addValues(TEST_IFACE, UID_ANOTHER_USER, SET_DEFAULT, TAG_NONE, METERED_NO,
+                        ROAMING_NO, BASE_BYTES + THRESHOLD_BYTES, 2L, BASE_BYTES + THRESHOLD_BYTES,
+                        2L, 0L);
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces,
                 VPN_INFO, TEST_START);
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkStatsServiceTest.java
index 94c6711..728eb73 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkStatsServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkStatsServiceTest.java
@@ -22,6 +22,9 @@
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.ConnectivityManager.TYPE_WIMAX;
 import static android.net.NetworkStats.IFACE_ALL;
+import static android.net.NetworkStats.METERED_ALL;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.METERED_YES;
 import static android.net.NetworkStats.ROAMING_ALL;
 import static android.net.NetworkStats.ROAMING_NO;
 import static android.net.NetworkStats.ROAMING_YES;
@@ -40,21 +43,21 @@
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 import static android.text.format.DateUtils.WEEK_IN_MILLIS;
+
 import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
-import static org.easymock.EasyMock.anyInt;
-import static org.easymock.EasyMock.anyLong;
-import static org.easymock.EasyMock.anyObject;
-import static org.easymock.EasyMock.capture;
-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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
 
 import android.app.AlarmManager;
-import android.app.IAlarmListener;
-import android.app.IAlarmManager;
-import android.app.PendingIntent;
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
 import android.content.Intent;
@@ -63,6 +66,7 @@
 import android.net.INetworkManagementEventObserver;
 import android.net.INetworkStatsSession;
 import android.net.LinkProperties;
+import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
 import android.net.NetworkInfo.DetailedState;
 import android.net.NetworkState;
@@ -80,23 +84,28 @@
 import android.os.MessageQueue.IdleHandler;
 import android.os.Message;
 import android.os.PowerManager;
-import android.os.WorkSource;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
 import android.telephony.TelephonyManager;
 import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.Suppress;
 import android.util.TrustedTime;
 
 import com.android.internal.net.VpnInfo;
-import com.android.server.BroadcastInterceptingContext;
+import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.server.net.NetworkStatsService;
 import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
 import com.android.server.net.NetworkStatsService.NetworkStatsSettings.Config;
 
 import libcore.io.IoUtils;
 
-import org.easymock.Capture;
-import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -106,11 +115,11 @@
 /**
  * Tests for {@link NetworkStatsService}.
  *
- * TODO: This test is really brittle, largely due to overly-strict use of Easymock.
- * Rewrite w/ Mockito.
+ * TODO: This test used to be really brittle because it used Easymock - it uses Mockito now, but
+ * still uses the Easymock structure, which could be simplified.
  */
-@LargeTest
-public class NetworkStatsServiceTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+public class NetworkStatsServiceTest {
     private static final String TAG = "NetworkStatsServiceTest";
 
     private static final String TEST_IFACE = "test0";
@@ -137,10 +146,12 @@
     private BroadcastInterceptingContext mServiceContext;
     private File mStatsDir;
 
-    private INetworkManagementService mNetManager;
-    private TrustedTime mTime;
-    private NetworkStatsSettings mSettings;
-    private IConnectivityManager mConnManager;
+    private @Mock INetworkManagementService mNetManager;
+    private @Mock TrustedTime mTime;
+    private @Mock NetworkStatsSettings mSettings;
+    private @Mock IConnectivityManager mConnManager;
+    private @Mock IBinder mBinder;
+    private @Mock AlarmManager mAlarmManager;
     private IdleableHandlerThread mHandlerThread;
     private Handler mHandler;
 
@@ -148,32 +159,24 @@
     private INetworkStatsSession mSession;
     private INetworkManagementEventObserver mNetworkObserver;
 
-    @Override
+    @Before
     public void setUp() throws Exception {
-        super.setUp();
+        MockitoAnnotations.initMocks(this);
+        final Context context = InstrumentationRegistry.getContext();
 
-        mServiceContext = new BroadcastInterceptingContext(getContext());
-        mStatsDir = getContext().getFilesDir();
+        mServiceContext = new BroadcastInterceptingContext(context);
+        mStatsDir = context.getFilesDir();
         if (mStatsDir.exists()) {
             IoUtils.deleteContents(mStatsDir);
         }
 
-        mNetManager = createMock(INetworkManagementService.class);
-
-        // TODO: Mock AlarmManager when migrating this test to Mockito.
-        AlarmManager alarmManager = (AlarmManager) mServiceContext
-                .getSystemService(Context.ALARM_SERVICE);
-        mTime = createMock(TrustedTime.class);
-        mSettings = createMock(NetworkStatsSettings.class);
-        mConnManager = createMock(IConnectivityManager.class);
-
         PowerManager powerManager = (PowerManager) mServiceContext.getSystemService(
                 Context.POWER_SERVICE);
         PowerManager.WakeLock wakeLock =
                 powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
 
         mService = new NetworkStatsService(
-                mServiceContext, mNetManager, alarmManager, wakeLock, mTime,
+                mServiceContext, mNetManager, mAlarmManager, wakeLock, mTime,
                 TelephonyManager.getDefault(), mSettings, new NetworkStatsObservers(),
                 mStatsDir, getBaseDir(mStatsDir));
         mHandlerThread = new IdleableHandlerThread("HandlerThread");
@@ -190,22 +193,20 @@
         expectNetworkStatsUidDetail(buildEmptyStats());
         expectSystemReady();
 
-        // catch INetworkManagementEventObserver during systemReady()
-        final Capture<INetworkManagementEventObserver> networkObserver = new Capture<
-                INetworkManagementEventObserver>();
-        mNetManager.registerObserver(capture(networkObserver));
-        expectLastCall().atLeastOnce();
-
-        replay();
         mService.systemReady();
         mSession = mService.openSession();
-        verifyAndReset();
+        assertNotNull("openSession() failed", mSession);
 
+
+        // catch INetworkManagementEventObserver during systemReady()
+        ArgumentCaptor<INetworkManagementEventObserver> networkObserver =
+              ArgumentCaptor.forClass(INetworkManagementEventObserver.class);
+        verify(mNetManager).registerObserver(networkObserver.capture());
         mNetworkObserver = networkObserver.getValue();
 
     }
 
-    @Override
+    @After
     public void tearDown() throws Exception {
         IoUtils.deleteContents(mStatsDir);
 
@@ -219,10 +220,9 @@
 
         mSession.close();
         mService = null;
-
-        super.tearDown();
     }
 
+    @Test
     public void testNetworkStatsWifi() throws Exception {
         // pretend that wifi network comes online; service should ask about full
         // network state, and poll any existing interfaces before updating.
@@ -231,15 +231,13 @@
         expectNetworkState(buildWifiState());
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
         expectBandwidthControlCheck();
 
-        replay();
         mService.forceUpdateIfaces();
 
         // verify service has empty history for wifi
         assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
-        verifyAndReset();
+
 
         // modify some number on wifi, and trigger poll event
         incrementCurrentTime(HOUR_IN_MILLIS);
@@ -248,14 +246,11 @@
         expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .addIfaceValues(TEST_IFACE, 1024L, 1L, 2048L, 2L));
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
-
-        replay();
         forcePollAndWaitForIdle();
 
         // verify service recorded history
         assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0);
-        verifyAndReset();
+
 
         // and bump forward again, with counters going higher. this is
         // important, since polling should correctly subtract last snapshot.
@@ -265,17 +260,14 @@
         expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .addIfaceValues(TEST_IFACE, 4096L, 4L, 8192L, 8L));
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
-
-        replay();
         forcePollAndWaitForIdle();
 
         // verify service recorded history
         assertNetworkTotal(sTemplateWifi, 4096L, 4L, 8192L, 8L, 0);
-        verifyAndReset();
 
     }
 
+    @Test
     public void testStatsRebootPersist() throws Exception {
         assertStatsFilesExist(false);
 
@@ -286,15 +278,13 @@
         expectNetworkState(buildWifiState());
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
         expectBandwidthControlCheck();
 
-        replay();
         mService.forceUpdateIfaces();
 
         // verify service has empty history for wifi
         assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
-        verifyAndReset();
+
 
         // modify some number on wifi, and trigger poll event
         incrementCurrentTime(HOUR_IN_MILLIS);
@@ -308,33 +298,28 @@
                 .addValues(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
                 .addValues(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
                 .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L));
-        expectNetworkStatsPoll();
-
         mService.setUidForeground(UID_RED, false);
         mService.incrementOperationCount(UID_RED, 0xFAAD, 4);
         mService.setUidForeground(UID_RED, true);
         mService.incrementOperationCount(UID_RED, 0xFAAD, 6);
 
-        replay();
         forcePollAndWaitForIdle();
 
         // verify service recorded history
         assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0);
         assertUidTotal(sTemplateWifi, UID_RED, 1024L, 8L, 512L, 4L, 10);
-        assertUidTotal(sTemplateWifi, UID_RED, SET_DEFAULT, ROAMING_NO, 512L, 4L, 256L, 2L, 4);
-        assertUidTotal(sTemplateWifi, UID_RED, SET_FOREGROUND, ROAMING_NO, 512L, 4L, 256L, 2L,
-                6);
+        assertUidTotal(sTemplateWifi, UID_RED, SET_DEFAULT, METERED_NO, ROAMING_NO, 512L, 4L, 256L,
+                2L, 4);
+        assertUidTotal(sTemplateWifi, UID_RED, SET_FOREGROUND, METERED_NO, ROAMING_NO, 512L, 4L,
+                256L, 2L, 6);
         assertUidTotal(sTemplateWifi, UID_BLUE, 128L, 1L, 128L, 1L, 0);
-        verifyAndReset();
+
 
         // graceful shutdown system, which should trigger persist of stats, and
         // clear any values in memory.
         expectCurrentTime();
         expectDefaultSettings();
-        replay();
         mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
-        verifyAndReset();
-
         assertStatsFilesExist(true);
 
         // boot through serviceReady() again
@@ -343,30 +328,22 @@
         expectNetworkStatsUidDetail(buildEmptyStats());
         expectSystemReady();
 
-        // catch INetworkManagementEventObserver during systemReady()
-        final Capture<INetworkManagementEventObserver> networkObserver = new Capture<
-                INetworkManagementEventObserver>();
-        mNetManager.registerObserver(capture(networkObserver));
-        expectLastCall().atLeastOnce();
-
-        replay();
         mService.systemReady();
 
-        mNetworkObserver = networkObserver.getValue();
-
         // after systemReady(), we should have historical stats loaded again
         assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0);
         assertUidTotal(sTemplateWifi, UID_RED, 1024L, 8L, 512L, 4L, 10);
-        assertUidTotal(sTemplateWifi, UID_RED, SET_DEFAULT, ROAMING_NO, 512L, 4L, 256L, 2L, 4);
-        assertUidTotal(sTemplateWifi, UID_RED, SET_FOREGROUND, ROAMING_NO, 512L, 4L, 256L, 2L,
-                6);
+        assertUidTotal(sTemplateWifi, UID_RED, SET_DEFAULT, METERED_NO, ROAMING_NO, 512L, 4L, 256L,
+                2L, 4);
+        assertUidTotal(sTemplateWifi, UID_RED, SET_FOREGROUND, METERED_NO, ROAMING_NO, 512L, 4L,
+                256L, 2L, 6);
         assertUidTotal(sTemplateWifi, UID_BLUE, 128L, 1L, 128L, 1L, 0);
-        verifyAndReset();
 
     }
 
     // TODO: simulate reboot to test bucket resize
-    @Suppress
+    @Test
+    @Ignore
     public void testStatsBucketResize() throws Exception {
         NetworkStatsHistory history = null;
 
@@ -379,12 +356,10 @@
         expectNetworkState(buildWifiState());
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
         expectBandwidthControlCheck();
 
-        replay();
         mService.forceUpdateIfaces();
-        verifyAndReset();
+
 
         // modify some number on wifi, and trigger poll event
         incrementCurrentTime(2 * HOUR_IN_MILLIS);
@@ -393,9 +368,6 @@
         expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .addIfaceValues(TEST_IFACE, 512L, 4L, 512L, 4L));
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
-
-        replay();
         forcePollAndWaitForIdle();
 
         // verify service recorded history
@@ -403,7 +375,7 @@
         assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, 512L, 4L, 512L, 4L, 0);
         assertEquals(HOUR_IN_MILLIS, history.getBucketDuration());
         assertEquals(2, history.size());
-        verifyAndReset();
+
 
         // now change bucket duration setting and trigger another poll with
         // exact same values, which should resize existing buckets.
@@ -411,9 +383,6 @@
         expectSettings(0L, 30 * MINUTE_IN_MILLIS, WEEK_IN_MILLIS);
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
-
-        replay();
         forcePollAndWaitForIdle();
 
         // verify identical stats, but spread across 4 buckets now
@@ -421,10 +390,10 @@
         assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, 512L, 4L, 512L, 4L, 0);
         assertEquals(30 * MINUTE_IN_MILLIS, history.getBucketDuration());
         assertEquals(4, history.size());
-        verifyAndReset();
 
     }
 
+    @Test
     public void testUidStatsAcrossNetworks() throws Exception {
         // pretend first mobile network comes online
         expectCurrentTime();
@@ -432,12 +401,10 @@
         expectNetworkState(buildMobile3gState(IMSI_1));
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
         expectBandwidthControlCheck();
 
-        replay();
         mService.forceUpdateIfaces();
-        verifyAndReset();
+
 
         // create some traffic on first network
         incrementCurrentTime(HOUR_IN_MILLIS);
@@ -449,11 +416,8 @@
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L)
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
                 .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 512L, 4L, 0L, 0L, 0L));
-        expectNetworkStatsPoll();
-
         mService.incrementOperationCount(UID_RED, 0xF00D, 10);
 
-        replay();
         forcePollAndWaitForIdle();
 
         // verify service recorded history
@@ -461,7 +425,7 @@
         assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
         assertUidTotal(sTemplateImsi1, UID_RED, 1536L, 12L, 512L, 4L, 10);
         assertUidTotal(sTemplateImsi1, UID_BLUE, 512L, 4L, 0L, 0L, 0);
-        verifyAndReset();
+
 
         // now switch networks; this also tests that we're okay with interfaces
         // disappearing, to verify we don't count backwards.
@@ -475,13 +439,11 @@
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L)
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
                 .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 512L, 4L, 0L, 0L, 0L));
-        expectNetworkStatsPoll();
         expectBandwidthControlCheck();
 
-        replay();
         mService.forceUpdateIfaces();
         forcePollAndWaitForIdle();
-        verifyAndReset();
+
 
         // create traffic on second network
         incrementCurrentTime(HOUR_IN_MILLIS);
@@ -494,11 +456,8 @@
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
                 .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 640L, 5L, 1024L, 8L, 0L)
                 .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, 0xFAAD, 128L, 1L, 1024L, 8L, 0L));
-        expectNetworkStatsPoll();
-
         mService.incrementOperationCount(UID_BLUE, 0xFAAD, 10);
 
-        replay();
         forcePollAndWaitForIdle();
 
         // verify original history still intact
@@ -511,10 +470,10 @@
         assertNetworkTotal(sTemplateImsi2, 128L, 1L, 1024L, 8L, 0);
         assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
         assertUidTotal(sTemplateImsi2, UID_BLUE, 128L, 1L, 1024L, 8L, 10);
-        verifyAndReset();
 
     }
 
+    @Test
     public void testUidRemovedIsMoved() throws Exception {
         // pretend that network comes online
         expectCurrentTime();
@@ -522,12 +481,10 @@
         expectNetworkState(buildWifiState());
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
         expectBandwidthControlCheck();
 
-        replay();
         mService.forceUpdateIfaces();
-        verifyAndReset();
+
 
         // create some traffic
         incrementCurrentTime(HOUR_IN_MILLIS);
@@ -540,11 +497,8 @@
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 16L, 1L, 16L, 1L, 0L)
                 .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 4096L, 258L, 512L, 32L, 0L)
                 .addValues(TEST_IFACE, UID_GREEN, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L));
-        expectNetworkStatsPoll();
-
         mService.incrementOperationCount(UID_RED, 0xFAAD, 10);
 
-        replay();
         forcePollAndWaitForIdle();
 
         // verify service recorded history
@@ -552,7 +506,7 @@
         assertUidTotal(sTemplateWifi, UID_RED, 16L, 1L, 16L, 1L, 10);
         assertUidTotal(sTemplateWifi, UID_BLUE, 4096L, 258L, 512L, 32L, 0);
         assertUidTotal(sTemplateWifi, UID_GREEN, 16L, 1L, 16L, 1L, 0);
-        verifyAndReset();
+
 
         // now pretend two UIDs are uninstalled, which should migrate stats to
         // special "removed" bucket.
@@ -565,9 +519,6 @@
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 16L, 1L, 16L, 1L, 0L)
                 .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 4096L, 258L, 512L, 32L, 0L)
                 .addValues(TEST_IFACE, UID_GREEN, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L));
-        expectNetworkStatsPoll();
-
-        replay();
         final Intent intent = new Intent(ACTION_UID_REMOVED);
         intent.putExtra(EXTRA_UID, UID_BLUE);
         mServiceContext.sendBroadcast(intent);
@@ -581,10 +532,10 @@
         assertUidTotal(sTemplateWifi, UID_BLUE, 0L, 0L, 0L, 0L, 0);
         assertUidTotal(sTemplateWifi, UID_GREEN, 16L, 1L, 16L, 1L, 0);
         assertUidTotal(sTemplateWifi, UID_REMOVED, 4112L, 259L, 528L, 33L, 10);
-        verifyAndReset();
 
     }
 
+    @Test
     public void testUid3g4gCombinedByTemplate() throws Exception {
         // pretend that network comes online
         expectCurrentTime();
@@ -592,12 +543,10 @@
         expectNetworkState(buildMobile3gState(IMSI_1));
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
         expectBandwidthControlCheck();
 
-        replay();
         mService.forceUpdateIfaces();
-        verifyAndReset();
+
 
         // create some traffic
         incrementCurrentTime(HOUR_IN_MILLIS);
@@ -607,16 +556,13 @@
         expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1024L, 8L, 1024L, 8L, 0L)
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L));
-        expectNetworkStatsPoll();
-
         mService.incrementOperationCount(UID_RED, 0xF00D, 5);
 
-        replay();
         forcePollAndWaitForIdle();
 
         // verify service recorded history
         assertUidTotal(sTemplateImsi1, UID_RED, 1024L, 8L, 1024L, 8L, 5);
-        verifyAndReset();
+
 
         // now switch over to 4g network
         incrementCurrentTime(HOUR_IN_MILLIS);
@@ -627,13 +573,11 @@
         expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1024L, 8L, 1024L, 8L, 0L)
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L));
-        expectNetworkStatsPoll();
         expectBandwidthControlCheck();
 
-        replay();
         mService.forceUpdateIfaces();
         forcePollAndWaitForIdle();
-        verifyAndReset();
+
 
         // create traffic on second network
         incrementCurrentTime(HOUR_IN_MILLIS);
@@ -645,19 +589,15 @@
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
                 .addValues(TEST_IFACE2, UID_RED, SET_DEFAULT, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
                 .addValues(TEST_IFACE2, UID_RED, SET_DEFAULT, 0xFAAD, 512L, 4L, 256L, 2L, 0L));
-        expectNetworkStatsPoll();
-
         mService.incrementOperationCount(UID_RED, 0xFAAD, 5);
 
-        replay();
         forcePollAndWaitForIdle();
 
         // verify that ALL_MOBILE template combines both
         assertUidTotal(sTemplateImsi1, UID_RED, 1536L, 12L, 1280L, 10L, 10);
-
-        verifyAndReset();
     }
 
+    @Test
     public void testSummaryForAllUid() throws Exception {
         // pretend that network comes online
         expectCurrentTime();
@@ -665,12 +605,10 @@
         expectNetworkState(buildWifiState());
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
         expectBandwidthControlCheck();
 
-        replay();
         mService.forceUpdateIfaces();
-        verifyAndReset();
+
 
         // create some traffic for two apps
         incrementCurrentTime(HOUR_IN_MILLIS);
@@ -681,17 +619,14 @@
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L)
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 10L, 1L, 10L, 1L, 0L)
                 .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0L));
-        expectNetworkStatsPoll();
-
         mService.incrementOperationCount(UID_RED, 0xF00D, 1);
 
-        replay();
         forcePollAndWaitForIdle();
 
         // verify service recorded history
         assertUidTotal(sTemplateWifi, UID_RED, 50L, 5L, 50L, 5L, 1);
         assertUidTotal(sTemplateWifi, UID_BLUE, 1024L, 8L, 512L, 4L, 0);
-        verifyAndReset();
+
 
         // now create more traffic in next hour, but only for one app
         incrementCurrentTime(HOUR_IN_MILLIS);
@@ -702,33 +637,29 @@
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L)
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 10L, 1L, 10L, 1L, 0L)
                 .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 2048L, 16L, 1024L, 8L, 0L));
-        expectNetworkStatsPoll();
-
-        replay();
         forcePollAndWaitForIdle();
 
         // first verify entire history present
         NetworkStats stats = mSession.getSummaryForAllUid(
                 sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true);
         assertEquals(3, stats.size());
-        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, ROAMING_NO, 50L, 5L,
-                50L, 5L, 1);
-        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, ROAMING_NO, 10L, 1L, 10L,
-                1L, 1);
-        assertValues(stats, IFACE_ALL, UID_BLUE, SET_DEFAULT, TAG_NONE, ROAMING_NO, 2048L, 16L,
-                1024L, 8L, 0);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 50L,
+                5L, 50L, 5L, 1);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, 10L,
+                1L, 10L, 1L, 1);
+        assertValues(stats, IFACE_ALL, UID_BLUE, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                2048L, 16L, 1024L, 8L, 0);
 
         // now verify that recent history only contains one uid
         final long currentTime = currentTimeMillis();
         stats = mSession.getSummaryForAllUid(
                 sTemplateWifi, currentTime - HOUR_IN_MILLIS, currentTime, true);
         assertEquals(1, stats.size());
-        assertValues(stats, IFACE_ALL, UID_BLUE, SET_DEFAULT, TAG_NONE, ROAMING_NO, 1024L, 8L,
-                512L, 4L, 0);
-
-        verifyAndReset();
+        assertValues(stats, IFACE_ALL, UID_BLUE, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                1024L, 8L, 512L, 4L, 0);
     }
 
+    @Test
     public void testForegroundBackground() throws Exception {
         // pretend that network comes online
         expectCurrentTime();
@@ -736,12 +667,10 @@
         expectNetworkState(buildWifiState());
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
         expectBandwidthControlCheck();
 
-        replay();
         mService.forceUpdateIfaces();
-        verifyAndReset();
+
 
         // create some initial traffic
         incrementCurrentTime(HOUR_IN_MILLIS);
@@ -751,16 +680,13 @@
         expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L)
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 64L, 1L, 64L, 1L, 0L));
-        expectNetworkStatsPoll();
-
         mService.incrementOperationCount(UID_RED, 0xF00D, 1);
 
-        replay();
         forcePollAndWaitForIdle();
 
         // verify service recorded history
         assertUidTotal(sTemplateWifi, UID_RED, 128L, 2L, 128L, 2L, 1);
-        verifyAndReset();
+
 
         // now switch to foreground
         incrementCurrentTime(HOUR_IN_MILLIS);
@@ -772,12 +698,9 @@
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 64L, 1L, 64L, 1L, 0L)
                 .addValues(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 32L, 2L, 32L, 2L, 0L)
                 .addValues(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 1L, 1L, 1L, 1L, 0L));
-        expectNetworkStatsPoll();
-
         mService.setUidForeground(UID_RED, true);
         mService.incrementOperationCount(UID_RED, 0xFAAD, 1);
 
-        replay();
         forcePollAndWaitForIdle();
 
         // test that we combined correctly
@@ -787,18 +710,59 @@
         final NetworkStats stats = mSession.getSummaryForAllUid(
                 sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true);
         assertEquals(4, stats.size());
-        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, ROAMING_NO, 128L, 2L,
-                128L, 2L, 1);
-        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, ROAMING_NO, 64L, 1L, 64L,
-                1L, 1);
-        assertValues(stats, IFACE_ALL, UID_RED, SET_FOREGROUND, TAG_NONE, ROAMING_NO, 32L, 2L,
-                32L, 2L, 1);
-        assertValues(stats, IFACE_ALL, UID_RED, SET_FOREGROUND, 0xFAAD, ROAMING_NO, 1L, 1L, 1L,
-                1L, 1);
-
-        verifyAndReset();
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 128L,
+                2L, 128L, 2L, 1);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, 64L,
+                1L, 64L, 1L, 1);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                32L, 2L, 32L, 2L, 1);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_FOREGROUND, 0xFAAD, METERED_NO, ROAMING_NO, 1L,
+                1L, 1L, 1L, 1);
     }
 
+    @Test
+    public void testMetered() throws Exception {
+        // pretend that network comes online
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkState(buildWifiState(true /* isMetered */));
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectBandwidthControlCheck();
+
+        mService.forceUpdateIfaces();
+
+
+        // create some initial traffic
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        // Note that all traffic from NetworkManagementService is tagged as METERED_NO and
+        // ROAMING_NO, because metered and roaming isn't tracked at that layer. We layer it
+        // on top by inspecting the iface properties.
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 128L,
+                        2L, 128L, 2L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, 64L,
+                        1L, 64L, 1L, 0L));
+        mService.incrementOperationCount(UID_RED, 0xF00D, 1);
+
+        forcePollAndWaitForIdle();
+
+        // verify service recorded history
+        assertUidTotal(sTemplateWifi, UID_RED, 128L, 2L, 128L, 2L, 1);
+        // verify entire history present
+        final NetworkStats stats = mSession.getSummaryForAllUid(
+                sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true);
+        assertEquals(2, stats.size());
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                128L, 2L, 128L, 2L, 1);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO, 64L,
+                1L, 64L, 1L, 1);
+    }
+
+    @Test
     public void testRoaming() throws Exception {
         // pretend that network comes online
         expectCurrentTime();
@@ -806,29 +770,24 @@
         expectNetworkState(buildMobile3gState(IMSI_1, true /* isRoaming */));
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
         expectBandwidthControlCheck();
 
-        replay();
         mService.forceUpdateIfaces();
-        verifyAndReset();
+
 
         // Create some traffic
         incrementCurrentTime(HOUR_IN_MILLIS);
         expectCurrentTime();
         expectDefaultSettings();
         expectNetworkStatsSummary(buildEmptyStats());
-        // Note that all traffic from NetworkManagementService is tagged as ROAMING_NO, because
-        // roaming isn't tracked at that layer. We layer it on top by inspecting the iface
-        // properties.
+        // Note that all traffic from NetworkManagementService is tagged as METERED_NO and
+        // ROAMING_NO, because metered and roaming isn't tracked at that layer. We layer it
+        // on top by inspecting the iface properties.
         expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
-                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, ROAMING_NO, 128L, 2L,
-                        128L, 2L, 0L)
-                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, ROAMING_NO, 64L, 1L, 64L,
-                        1L, 0L));
-        expectNetworkStatsPoll();
-
-        replay();
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_ALL, ROAMING_NO,
+                        128L, 2L, 128L, 2L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, METERED_ALL, ROAMING_NO, 64L,
+                        1L, 64L, 1L, 0L));
         forcePollAndWaitForIdle();
 
         // verify service recorded history
@@ -838,14 +797,13 @@
         final NetworkStats stats = mSession.getSummaryForAllUid(
                 sTemplateImsi1, Long.MIN_VALUE, Long.MAX_VALUE, true);
         assertEquals(2, stats.size());
-        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, ROAMING_YES, 128L, 2L,
-                128L, 2L, 0);
-        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, ROAMING_YES, 64L, 1L, 64L,
-                1L, 0);
-
-        verifyAndReset();
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_ALL, ROAMING_YES,
+                128L, 2L, 128L, 2L, 0);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_ALL, ROAMING_YES, 64L,
+                1L, 64L, 1L, 0);
     }
 
+    @Test
     public void testTethering() throws Exception {
         // pretend first mobile network comes online
         expectCurrentTime();
@@ -853,12 +811,10 @@
         expectNetworkState(buildMobile3gState(IMSI_1));
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
         expectBandwidthControlCheck();
 
-        replay();
         mService.forceUpdateIfaces();
-        verifyAndReset();
+
 
         // create some tethering traffic
         incrementCurrentTime(HOUR_IN_MILLIS);
@@ -871,22 +827,20 @@
                 .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L);
         final String[] tetherIfacePairs = new String[] { TEST_IFACE, "wlan0" };
         final NetworkStats tetherStats = new NetworkStats(getElapsedRealtime(), 1)
-                .addValues(TEST_IFACE, UID_TETHERING, SET_DEFAULT, TAG_NONE, 1920L, 14L, 384L, 2L, 0L);
+                .addValues(TEST_IFACE, UID_TETHERING, SET_DEFAULT, TAG_NONE, 1920L, 14L, 384L, 2L,
+                        0L);
 
         expectNetworkStatsUidDetail(uidStats, tetherIfacePairs, tetherStats);
-        expectNetworkStatsPoll();
-
-        replay();
         forcePollAndWaitForIdle();
 
         // verify service recorded history
         assertNetworkTotal(sTemplateImsi1, 2048L, 16L, 512L, 4L, 0);
         assertUidTotal(sTemplateImsi1, UID_RED, 128L, 2L, 128L, 2L, 0);
         assertUidTotal(sTemplateImsi1, UID_TETHERING, 1920L, 14L, 384L, 2L, 0);
-        verifyAndReset();
 
     }
 
+    @Test
     public void testRegisterUsageCallback() throws Exception {
         // pretend that wifi network comes online; service should ask about full
         // network state, and poll any existing interfaces before updating.
@@ -895,16 +849,12 @@
         expectNetworkState(buildWifiState());
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
         expectBandwidthControlCheck();
 
-        replay();
         mService.forceUpdateIfaces();
 
         // verify service has empty history for wifi
         assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
-        verifyAndReset();
-
         String callingPackage = "the.calling.package";
         long thresholdInBytes = 1L;  // very small; should be overriden by framework
         DataUsageRequest inputRequest = new DataUsageRequest(
@@ -915,23 +865,18 @@
         LatchedHandler latchedHandler = new LatchedHandler(Looper.getMainLooper(), cv);
         Messenger messenger = new Messenger(latchedHandler);
 
-        // Allow binder to connect
-        IBinder mockBinder = createMock(IBinder.class);
-        mockBinder.linkToDeath((IBinder.DeathRecipient) anyObject(), anyInt());
-        EasyMock.replay(mockBinder);
-
         // Force poll
         expectCurrentTime();
         expectDefaultSettings();
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
-        replay();
+
+
 
         // Register and verify request and that binder was called
         DataUsageRequest request =
                 mService.registerUsageCallback(callingPackage, inputRequest,
-                        messenger, mockBinder);
+                        messenger, mBinder);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request.template));
         long minThresholdInBytes = 2 * 1024 * 1024; // 2 MB
@@ -941,11 +886,11 @@
         mHandler.sendMessage(mHandler.obtainMessage(-1));
         mHandlerThread.waitForIdle(WAIT_TIMEOUT);
 
-        verifyAndReset();
+
 
         // Make sure that the caller binder gets connected
-        EasyMock.verify(mockBinder);
-        EasyMock.reset(mockBinder);
+        verify(mBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+
 
         // modify some number on wifi, and trigger poll event
         // not enough traffic to call data usage callback
@@ -955,13 +900,9 @@
         expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .addIfaceValues(TEST_IFACE, 1024L, 1L, 2048L, 2L));
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
-
-        replay();
         forcePollAndWaitForIdle();
 
         // verify service recorded history
-        verifyAndReset();
         assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0);
 
         // make sure callback has not being called
@@ -975,14 +916,11 @@
         expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
                 .addIfaceValues(TEST_IFACE, 4096000L, 4L, 8192000L, 8L));
         expectNetworkStatsUidDetail(buildEmptyStats());
-        expectNetworkStatsPoll();
-
-        replay();
         forcePollAndWaitForIdle();
 
         // verify service recorded history
         assertNetworkTotal(sTemplateWifi, 4096000L, 4L, 8192000L, 8L, 0);
-        verifyAndReset();
+
 
         // Wait for the caller to ack receipt of CALLBACK_LIMIT_REACHED
         assertTrue(cv.block(WAIT_TIMEOUT));
@@ -990,9 +928,7 @@
         cv.close();
 
         // Allow binder to disconnect
-        expect(mockBinder.unlinkToDeath((IBinder.DeathRecipient) anyObject(), anyInt()))
-                .andReturn(true);
-        EasyMock.replay(mockBinder);
+        when(mBinder.unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt())).thenReturn(true);
 
         // Unregister request
         mService.unregisterUsageRequest(request);
@@ -1002,9 +938,10 @@
         assertEquals(NetworkStatsManager.CALLBACK_RELEASED, latchedHandler.mLastMessageType);
 
         // Make sure that the caller binder gets disconnected
-        EasyMock.verify(mockBinder);
+        verify(mBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
     }
 
+    @Test
     public void testUnregisterUsageCallback_unknown_noop() throws Exception {
         String callingPackage = "the.calling.package";
         long thresholdInBytes = 10 * 1024 * 1024;  // 10 MB
@@ -1034,18 +971,18 @@
 
         // verify summary API
         final NetworkStats stats = mSession.getSummaryForNetwork(template, start, end);
-        assertValues(stats, IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, ROAMING_NO, rxBytes,
-                rxPackets, txBytes, txPackets, operations);
+        assertValues(stats, IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_ALL, ROAMING_NO,
+                rxBytes, rxPackets, txBytes, txPackets, operations);
     }
 
     private void assertUidTotal(NetworkTemplate template, int uid, long rxBytes, long rxPackets,
             long txBytes, long txPackets, int operations) throws Exception {
-        assertUidTotal(template, uid, SET_ALL, ROAMING_ALL, rxBytes, rxPackets, txBytes, txPackets,
-                operations);
+        assertUidTotal(template, uid, SET_ALL, METERED_ALL, ROAMING_ALL, rxBytes, rxPackets,
+                txBytes, txPackets, operations);
     }
 
-    private void assertUidTotal(NetworkTemplate template, int uid, int set, int roaming,
-            long rxBytes, long rxPackets, long txBytes, long txPackets, int operations)
+    private void assertUidTotal(NetworkTemplate template, int uid, int set, int metered,
+            int roaming, long rxBytes, long rxPackets, long txBytes, long txPackets, int operations)
             throws Exception {
         // verify history API
         final NetworkStatsHistory history = mSession.getHistoryForUid(
@@ -1056,38 +993,35 @@
         // verify summary API
         final NetworkStats stats = mSession.getSummaryForAllUid(
                 template, Long.MIN_VALUE, Long.MAX_VALUE, false);
-        assertValues(stats, IFACE_ALL, uid, set, TAG_NONE, roaming, rxBytes, rxPackets, txBytes,
-                txPackets, operations);
+        assertValues(stats, IFACE_ALL, uid, set, TAG_NONE, metered, roaming, rxBytes, rxPackets,
+                txBytes, txPackets, operations);
     }
 
     private void expectSystemReady() throws Exception {
-        mNetManager.setGlobalAlert(anyLong());
-        expectLastCall().atLeastOnce();
-
         expectNetworkStatsSummary(buildEmptyStats());
         expectBandwidthControlCheck();
     }
 
     private void expectNetworkState(NetworkState... state) throws Exception {
-        expect(mConnManager.getAllNetworkState()).andReturn(state).atLeastOnce();
+        when(mConnManager.getAllNetworkState()).thenReturn(state);
 
         final LinkProperties linkProp = state.length > 0 ? state[0].linkProperties : null;
-        expect(mConnManager.getActiveLinkProperties()).andReturn(linkProp).atLeastOnce();
+        when(mConnManager.getActiveLinkProperties()).thenReturn(linkProp);
     }
 
     private void expectNetworkStatsSummary(NetworkStats summary) throws Exception {
-        expect(mConnManager.getAllVpnInfo()).andReturn(new VpnInfo[0]).atLeastOnce();
+        when(mConnManager.getAllVpnInfo()).thenReturn(new VpnInfo[0]);
 
         expectNetworkStatsSummaryDev(summary);
         expectNetworkStatsSummaryXt(summary);
     }
 
     private void expectNetworkStatsSummaryDev(NetworkStats summary) throws Exception {
-        expect(mNetManager.getNetworkStatsSummaryDev()).andReturn(summary).atLeastOnce();
+        when(mNetManager.getNetworkStatsSummaryDev()).thenReturn(summary);
     }
 
     private void expectNetworkStatsSummaryXt(NetworkStats summary) throws Exception {
-        expect(mNetManager.getNetworkStatsSummaryXt()).andReturn(summary).atLeastOnce();
+        when(mNetManager.getNetworkStatsSummaryXt()).thenReturn(summary);
     }
 
     private void expectNetworkStatsUidDetail(NetworkStats detail) throws Exception {
@@ -1097,11 +1031,10 @@
     private void expectNetworkStatsUidDetail(
             NetworkStats detail, String[] tetherIfacePairs, NetworkStats tetherStats)
             throws Exception {
-        expect(mNetManager.getNetworkStatsUidDetail(eq(UID_ALL))).andReturn(detail).atLeastOnce();
+        when(mNetManager.getNetworkStatsUidDetail(UID_ALL)).thenReturn(detail);
 
         // also include tethering details, since they are folded into UID
-        expect(mNetManager.getNetworkStatsTethering())
-                .andReturn(tetherStats).atLeastOnce();
+        when(mNetManager.getNetworkStatsTethering()).thenReturn(tetherStats);
     }
 
     private void expectDefaultSettings() throws Exception {
@@ -1110,38 +1043,33 @@
 
     private void expectSettings(long persistBytes, long bucketDuration, long deleteAge)
             throws Exception {
-        expect(mSettings.getPollInterval()).andReturn(HOUR_IN_MILLIS).anyTimes();
-        expect(mSettings.getTimeCacheMaxAge()).andReturn(DAY_IN_MILLIS).anyTimes();
-        expect(mSettings.getSampleEnabled()).andReturn(true).anyTimes();
+        when(mSettings.getPollInterval()).thenReturn(HOUR_IN_MILLIS);
+        when(mSettings.getTimeCacheMaxAge()).thenReturn(DAY_IN_MILLIS);
+        when(mSettings.getSampleEnabled()).thenReturn(true);
 
         final Config config = new Config(bucketDuration, deleteAge, deleteAge);
-        expect(mSettings.getDevConfig()).andReturn(config).anyTimes();
-        expect(mSettings.getXtConfig()).andReturn(config).anyTimes();
-        expect(mSettings.getUidConfig()).andReturn(config).anyTimes();
-        expect(mSettings.getUidTagConfig()).andReturn(config).anyTimes();
+        when(mSettings.getDevConfig()).thenReturn(config);
+        when(mSettings.getXtConfig()).thenReturn(config);
+        when(mSettings.getUidConfig()).thenReturn(config);
+        when(mSettings.getUidTagConfig()).thenReturn(config);
 
-        expect(mSettings.getGlobalAlertBytes(anyLong())).andReturn(MB_IN_BYTES).anyTimes();
-        expect(mSettings.getDevPersistBytes(anyLong())).andReturn(MB_IN_BYTES).anyTimes();
-        expect(mSettings.getXtPersistBytes(anyLong())).andReturn(MB_IN_BYTES).anyTimes();
-        expect(mSettings.getUidPersistBytes(anyLong())).andReturn(MB_IN_BYTES).anyTimes();
-        expect(mSettings.getUidTagPersistBytes(anyLong())).andReturn(MB_IN_BYTES).anyTimes();
+        when(mSettings.getGlobalAlertBytes(anyLong())).thenReturn(MB_IN_BYTES);
+        when(mSettings.getDevPersistBytes(anyLong())).thenReturn(MB_IN_BYTES);
+        when(mSettings.getXtPersistBytes(anyLong())).thenReturn(MB_IN_BYTES);
+        when(mSettings.getUidPersistBytes(anyLong())).thenReturn(MB_IN_BYTES);
+        when(mSettings.getUidTagPersistBytes(anyLong())).thenReturn(MB_IN_BYTES);
     }
 
     private void expectCurrentTime() throws Exception {
-        expect(mTime.forceRefresh()).andReturn(false).anyTimes();
-        expect(mTime.hasCache()).andReturn(true).anyTimes();
-        expect(mTime.currentTimeMillis()).andReturn(currentTimeMillis()).anyTimes();
-        expect(mTime.getCacheAge()).andReturn(0L).anyTimes();
-        expect(mTime.getCacheCertainty()).andReturn(0L).anyTimes();
-    }
-
-    private void expectNetworkStatsPoll() throws Exception {
-        mNetManager.setGlobalAlert(anyLong());
-        expectLastCall().anyTimes();
+        when(mTime.forceRefresh()).thenReturn(false);
+        when(mTime.hasCache()).thenReturn(true);
+        when(mTime.currentTimeMillis()).thenReturn(currentTimeMillis());
+        when(mTime.getCacheAge()).thenReturn(0L);
+        when(mTime.getCacheCertainty()).thenReturn(0L);
     }
 
     private void expectBandwidthControlCheck() throws Exception {
-        expect(mNetManager.isBandwidthControlEnabled()).andReturn(true).atLeastOnce();
+        when(mNetManager.isBandwidthControlEnabled()).thenReturn(true);
     }
 
     private void assertStatsFilesExist(boolean exist) {
@@ -1154,8 +1082,8 @@
     }
 
     private static void assertValues(NetworkStats stats, String iface, int uid, int set,
-            int tag, int roaming, long rxBytes, long rxPackets, long txBytes, long txPackets,
-            int operations) {
+            int tag, int metered, int roaming, long rxBytes, long rxPackets, long txBytes,
+            long txPackets, int operations) {
         final NetworkStats.Entry entry = new NetworkStats.Entry();
         List<Integer> sets = new ArrayList<>();
         if (set == SET_DEFAULT || set == SET_ALL) {
@@ -1173,11 +1101,21 @@
             roamings.add(ROAMING_YES);
         }
 
+        List<Integer> meterings = new ArrayList<>();
+        if (metered == METERED_NO || metered == METERED_ALL) {
+            meterings.add(METERED_NO);
+        }
+        if (metered == METERED_YES || metered == METERED_ALL) {
+            meterings.add(METERED_YES);
+        }
+
         for (int s : sets) {
             for (int r : roamings) {
-                final int i = stats.findIndex(iface, uid, s, tag, r);
-                if (i != -1) {
-                    entry.add(stats.getValues(i, null));
+                for (int m : meterings) {
+                    final int i = stats.findIndex(iface, uid, s, tag, m, r);
+                    if (i != -1) {
+                        entry.add(stats.getValues(i, null));
+                    }
                 }
             }
         }
@@ -1200,11 +1138,19 @@
     }
 
     private static NetworkState buildWifiState() {
+        return buildWifiState(false);
+    }
+
+    private static NetworkState buildWifiState(boolean isMetered) {
         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, null, null, TEST_SSID);
+        final NetworkCapabilities capabilities = new NetworkCapabilities();
+        if (!isMetered) {
+            capabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+        }
+        return new NetworkState(info, prop, capabilities, null, null, TEST_SSID);
     }
 
     private static NetworkState buildMobile3gState(String subscriberId) {
@@ -1218,7 +1164,8 @@
         info.setRoaming(isRoaming);
         final LinkProperties prop = new LinkProperties();
         prop.setInterfaceName(TEST_IFACE);
-        return new NetworkState(info, prop, null, null, subscriberId, null);
+        final NetworkCapabilities capabilities = new NetworkCapabilities();
+        return new NetworkState(info, prop, capabilities, null, subscriberId, null);
     }
 
     private static NetworkState buildMobile4gState(String iface) {
@@ -1226,7 +1173,8 @@
         info.setDetailedState(DetailedState.CONNECTED, null, null);
         final LinkProperties prop = new LinkProperties();
         prop.setInterfaceName(iface);
-        return new NetworkState(info, prop, null, null, null, null);
+        final NetworkCapabilities capabilities = new NetworkCapabilities();
+        return new NetworkState(info, prop, capabilities, null, null, null);
     }
 
     private NetworkStats buildEmptyStats() {
@@ -1249,15 +1197,6 @@
         mElapsedRealtime += duration;
     }
 
-    private void replay() {
-        EasyMock.replay(mNetManager, mTime, mSettings, mConnManager);
-    }
-
-    private void verifyAndReset() {
-        EasyMock.verify(mNetManager, mTime, mSettings, mConnManager);
-        EasyMock.reset(mNetManager, mTime, mSettings, mConnManager);
-    }
-
     private void forcePollAndWaitForIdle() {
         mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
         // Send dummy message to make sure that any previous message has been handled
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
new file mode 100644
index 0000000..46b6403
--- /dev/null
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -0,0 +1,2848 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.TYPE_ETHERNET;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.ConnectivityManager.getNetworkTypeName;
+import static android.net.NetworkCapabilities.*;
+
+import static org.mockito.Mockito.mock;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.ConnectivityManager.PacketKeepalive;
+import android.net.ConnectivityManager.PacketKeepaliveCallback;
+import android.net.INetworkPolicyManager;
+import android.net.INetworkStatsService;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.NetworkCapabilities;
+import android.net.NetworkConfig;
+import android.net.NetworkFactory;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkMisc;
+import android.net.NetworkRequest;
+import android.net.RouteInfo;
+import android.net.metrics.IpConnectivityLog;
+import android.net.util.AvoidBadWifiTracker;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.Messenger;
+import android.os.MessageQueue.IdleHandler;
+import android.os.Process;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.test.AndroidTestCase;
+import android.test.FlakyTest;
+import android.test.mock.MockContentResolver;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+import android.util.LogPrinter;
+
+import com.android.internal.util.WakeupMessage;
+import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.server.connectivity.NetworkAgentInfo;
+import com.android.server.connectivity.NetworkMonitor;
+import com.android.server.connectivity.NetworkMonitor.CaptivePortalProbeResult;
+import com.android.server.net.NetworkPinner;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BooleanSupplier;
+
+/**
+ * Tests for {@link ConnectivityService}.
+ *
+ * Build, install and run with:
+ *  runtest frameworks-services -c com.android.server.ConnectivityServiceTest
+ */
+public class ConnectivityServiceTest extends AndroidTestCase {
+    private static final String TAG = "ConnectivityServiceTest";
+
+    private static final int TIMEOUT_MS = 500;
+    private static final int TEST_LINGER_DELAY_MS = 120;
+
+    private BroadcastInterceptingContext mServiceContext;
+    private WrappedConnectivityService mService;
+    private WrappedConnectivityManager mCm;
+    private MockNetworkAgent mWiFiNetworkAgent;
+    private MockNetworkAgent mCellNetworkAgent;
+    private MockNetworkAgent mEthernetNetworkAgent;
+
+    // This class exists to test bindProcessToNetwork and getBoundNetworkForProcess. These methods
+    // do not go through ConnectivityService but talk to netd directly, so they don't automatically
+    // reflect the state of our test ConnectivityService.
+    private class WrappedConnectivityManager extends ConnectivityManager {
+        private Network mFakeBoundNetwork;
+
+        public synchronized boolean bindProcessToNetwork(Network network) {
+            mFakeBoundNetwork = network;
+            return true;
+        }
+
+        public synchronized Network getBoundNetworkForProcess() {
+            return mFakeBoundNetwork;
+        }
+
+        public WrappedConnectivityManager(Context context, ConnectivityService service) {
+            super(context, service);
+        }
+    }
+
+    private class MockContext extends BroadcastInterceptingContext {
+        private final MockContentResolver mContentResolver;
+
+        MockContext(Context base) {
+            super(base);
+            mContentResolver = new MockContentResolver();
+            mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+        }
+
+        @Override
+        public Object getSystemService(String name) {
+            if (Context.CONNECTIVITY_SERVICE.equals(name)) return mCm;
+            if (Context.NOTIFICATION_SERVICE.equals(name)) return mock(NotificationManager.class);
+            return super.getSystemService(name);
+        }
+
+        @Override
+        public ContentResolver getContentResolver() {
+            return mContentResolver;
+        }
+    }
+
+    /**
+     * A subclass of HandlerThread that allows callers to wait for it to become idle. waitForIdle
+     * will return immediately if the handler is already idle.
+     */
+    private class IdleableHandlerThread extends HandlerThread {
+        private IdleHandler mIdleHandler;
+
+        public IdleableHandlerThread(String name) {
+            super(name);
+        }
+
+        public void waitForIdle(int timeoutMs) {
+            final ConditionVariable cv = new ConditionVariable();
+            final MessageQueue queue = getLooper().getQueue();
+
+            synchronized (queue) {
+                if (queue.isIdle()) {
+                    return;
+                }
+
+                assertNull("BUG: only one idle handler allowed", mIdleHandler);
+                mIdleHandler = new IdleHandler() {
+                    public boolean queueIdle() {
+                        synchronized (queue) {
+                            cv.open();
+                            mIdleHandler = null;
+                            return false;  // Remove the handler.
+                        }
+                    }
+                };
+                queue.addIdleHandler(mIdleHandler);
+            }
+
+            if (!cv.block(timeoutMs)) {
+                fail("HandlerThread " + getName() +
+                        " did not become idle after " + timeoutMs + " ms");
+                queue.removeIdleHandler(mIdleHandler);
+            }
+        }
+    }
+
+    // Tests that IdleableHandlerThread works as expected.
+    @SmallTest
+    public void testIdleableHandlerThread() {
+        final int attempts = 50;  // Causes the test to take about 200ms on bullhead-eng.
+
+        // Tests that waitForIdle returns immediately if the service is already idle.
+        for (int i = 0; i < attempts; i++) {
+            mService.waitForIdle();
+        }
+
+        // Bring up a network that we can use to send messages to ConnectivityService.
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        waitFor(cv);
+        Network n = mWiFiNetworkAgent.getNetwork();
+        assertNotNull(n);
+
+        // Tests that calling waitForIdle waits for messages to be processed.
+        for (int i = 0; i < attempts; i++) {
+            mWiFiNetworkAgent.setSignalStrength(i);
+            mService.waitForIdle();
+            assertEquals(i, mCm.getNetworkCapabilities(n).getSignalStrength());
+        }
+    }
+
+    @SmallTest
+    @FlakyTest(tolerance = 3)
+    public void testNotWaitingForIdleCausesRaceConditions() {
+        // Bring up a network that we can use to send messages to ConnectivityService.
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        waitFor(cv);
+        Network n = mWiFiNetworkAgent.getNetwork();
+        assertNotNull(n);
+
+        // Ensure that not calling waitForIdle causes a race condition.
+        final int attempts = 50;  // Causes the test to take about 200ms on bullhead-eng.
+        for (int i = 0; i < attempts; i++) {
+            mWiFiNetworkAgent.setSignalStrength(i);
+            if (i != mCm.getNetworkCapabilities(n).getSignalStrength()) {
+                // We hit a race condition, as expected. Pass the test.
+                return;
+            }
+        }
+
+        // No race? There is a bug in this test.
+        fail("expected race condition at least once in " + attempts + " attempts");
+    }
+
+    private class MockNetworkAgent {
+        private final WrappedNetworkMonitor mWrappedNetworkMonitor;
+        private final NetworkInfo mNetworkInfo;
+        private final NetworkCapabilities mNetworkCapabilities;
+        private final IdleableHandlerThread mHandlerThread;
+        private final ConditionVariable mDisconnected = new ConditionVariable();
+        private final ConditionVariable mNetworkStatusReceived = new ConditionVariable();
+        private final ConditionVariable mPreventReconnectReceived = new ConditionVariable();
+        private int mScore;
+        private NetworkAgent mNetworkAgent;
+        private int mStartKeepaliveError = PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED;
+        private int mStopKeepaliveError = PacketKeepalive.NO_KEEPALIVE;
+        private Integer mExpectedKeepaliveSlot = null;
+        // Contains the redirectUrl from networkStatus(). Before reading, wait for
+        // mNetworkStatusReceived.
+        private String mRedirectUrl;
+
+        MockNetworkAgent(int transport) {
+            final int type = transportToLegacyType(transport);
+            final String typeName = ConnectivityManager.getNetworkTypeName(type);
+            mNetworkInfo = new NetworkInfo(type, 0, typeName, "Mock");
+            mNetworkCapabilities = new NetworkCapabilities();
+            mNetworkCapabilities.addTransportType(transport);
+            switch (transport) {
+                case TRANSPORT_ETHERNET:
+                    mScore = 70;
+                    break;
+                case TRANSPORT_WIFI:
+                    mScore = 60;
+                    break;
+                case TRANSPORT_CELLULAR:
+                    mScore = 50;
+                    break;
+                default:
+                    throw new UnsupportedOperationException("unimplemented network type");
+            }
+            mHandlerThread = new IdleableHandlerThread("Mock-" + typeName);
+            mHandlerThread.start();
+            mNetworkAgent = new NetworkAgent(mHandlerThread.getLooper(), mServiceContext,
+                    "Mock-" + typeName, mNetworkInfo, mNetworkCapabilities,
+                    new LinkProperties(), mScore, new NetworkMisc()) {
+                @Override
+                public void unwanted() { mDisconnected.open(); }
+
+                @Override
+                public void startPacketKeepalive(Message msg) {
+                    int slot = msg.arg1;
+                    if (mExpectedKeepaliveSlot != null) {
+                        assertEquals((int) mExpectedKeepaliveSlot, slot);
+                    }
+                    onPacketKeepaliveEvent(slot, mStartKeepaliveError);
+                }
+
+                @Override
+                public void stopPacketKeepalive(Message msg) {
+                    onPacketKeepaliveEvent(msg.arg1, mStopKeepaliveError);
+                }
+
+                @Override
+                public void networkStatus(int status, String redirectUrl) {
+                    mRedirectUrl = redirectUrl;
+                    mNetworkStatusReceived.open();
+                }
+
+                @Override
+                protected void preventAutomaticReconnect() {
+                    mPreventReconnectReceived.open();
+                }
+            };
+            // Waits for the NetworkAgent to be registered, which includes the creation of the
+            // NetworkMonitor.
+            mService.waitForIdle();
+            mWrappedNetworkMonitor = mService.getLastCreatedWrappedNetworkMonitor();
+        }
+
+        public void waitForIdle(int timeoutMs) {
+            mHandlerThread.waitForIdle(timeoutMs);
+        }
+
+        public void waitForIdle() {
+            waitForIdle(TIMEOUT_MS);
+        }
+
+        public void adjustScore(int change) {
+            mScore += change;
+            mNetworkAgent.sendNetworkScore(mScore);
+        }
+
+        public void addCapability(int capability) {
+            mNetworkCapabilities.addCapability(capability);
+            mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
+        }
+
+        public void removeCapability(int capability) {
+            mNetworkCapabilities.removeCapability(capability);
+            mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
+        }
+
+        public void setSignalStrength(int signalStrength) {
+            mNetworkCapabilities.setSignalStrength(signalStrength);
+            mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
+        }
+
+        public void connectWithoutInternet() {
+            mNetworkInfo.setDetailedState(DetailedState.CONNECTED, null, null);
+            mNetworkAgent.sendNetworkInfo(mNetworkInfo);
+        }
+
+        /**
+         * Transition this NetworkAgent to CONNECTED state with NET_CAPABILITY_INTERNET.
+         * @param validated Indicate if network should pretend to be validated.
+         */
+        public void connect(boolean validated) {
+            assertEquals("MockNetworkAgents can only be connected once",
+                    mNetworkInfo.getDetailedState(), DetailedState.IDLE);
+            assertFalse(mNetworkCapabilities.hasCapability(NET_CAPABILITY_INTERNET));
+
+            NetworkCallback callback = null;
+            final ConditionVariable validatedCv = new ConditionVariable();
+            if (validated) {
+                mWrappedNetworkMonitor.gen204ProbeResult = 204;
+                NetworkRequest request = new NetworkRequest.Builder()
+                        .addTransportType(mNetworkCapabilities.getTransportTypes()[0])
+                        .build();
+                callback = new NetworkCallback() {
+                    public void onCapabilitiesChanged(Network network,
+                            NetworkCapabilities networkCapabilities) {
+                        if (network.equals(getNetwork()) &&
+                            networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED)) {
+                            validatedCv.open();
+                        }
+                    }
+                };
+                mCm.registerNetworkCallback(request, callback);
+            }
+            addCapability(NET_CAPABILITY_INTERNET);
+
+            connectWithoutInternet();
+
+            if (validated) {
+                // Wait for network to validate.
+                waitFor(validatedCv);
+                mWrappedNetworkMonitor.gen204ProbeResult = 500;
+            }
+
+            if (callback != null) mCm.unregisterNetworkCallback(callback);
+        }
+
+        public void connectWithCaptivePortal(String redirectUrl) {
+            mWrappedNetworkMonitor.gen204ProbeResult = 200;
+            mWrappedNetworkMonitor.gen204ProbeRedirectUrl = redirectUrl;
+            connect(false);
+        }
+
+        public void disconnect() {
+            mNetworkInfo.setDetailedState(DetailedState.DISCONNECTED, null, null);
+            mNetworkAgent.sendNetworkInfo(mNetworkInfo);
+        }
+
+        public Network getNetwork() {
+            return new Network(mNetworkAgent.netId);
+        }
+
+        public ConditionVariable getPreventReconnectReceived() {
+            return mPreventReconnectReceived;
+        }
+
+        public ConditionVariable getDisconnectedCV() {
+            return mDisconnected;
+        }
+
+        public WrappedNetworkMonitor getWrappedNetworkMonitor() {
+            return mWrappedNetworkMonitor;
+        }
+
+        public void sendLinkProperties(LinkProperties lp) {
+            mNetworkAgent.sendLinkProperties(lp);
+        }
+
+        public void setStartKeepaliveError(int error) {
+            mStartKeepaliveError = error;
+        }
+
+        public void setStopKeepaliveError(int error) {
+            mStopKeepaliveError = error;
+        }
+
+        public void setExpectedKeepaliveSlot(Integer slot) {
+            mExpectedKeepaliveSlot = slot;
+        }
+
+        public String waitForRedirectUrl() {
+            assertTrue(mNetworkStatusReceived.block(TIMEOUT_MS));
+            return mRedirectUrl;
+        }
+    }
+
+    /**
+     * A NetworkFactory that allows tests to wait until any in-flight NetworkRequest add or remove
+     * operations have been processed. Before ConnectivityService can add or remove any requests,
+     * the factory must be told to expect those operations by calling expectAddRequests or
+     * expectRemoveRequests.
+     */
+    private static class MockNetworkFactory extends NetworkFactory {
+        private final ConditionVariable mNetworkStartedCV = new ConditionVariable();
+        private final ConditionVariable mNetworkStoppedCV = new ConditionVariable();
+        private final AtomicBoolean mNetworkStarted = new AtomicBoolean(false);
+
+        // Used to expect that requests be removed or added on a separate thread, without sleeping.
+        // Callers can call either expectAddRequests() or expectRemoveRequests() exactly once, then
+        // cause some other thread to add or remove requests, then call waitForRequests(). We can
+        // either expect requests to be added or removed, but not both, because CountDownLatch can
+        // only count in one direction.
+        private CountDownLatch mExpectations;
+
+        // Whether we are currently expecting requests to be added or removed. Valid only if
+        // mExpectations is non-null.
+        private boolean mExpectingAdditions;
+
+        public MockNetworkFactory(Looper looper, Context context, String logTag,
+                NetworkCapabilities filter) {
+            super(looper, context, logTag, filter);
+        }
+
+        public int getMyRequestCount() {
+            return getRequestCount();
+        }
+
+        protected void startNetwork() {
+            mNetworkStarted.set(true);
+            mNetworkStartedCV.open();
+        }
+
+        protected void stopNetwork() {
+            mNetworkStarted.set(false);
+            mNetworkStoppedCV.open();
+        }
+
+        public boolean getMyStartRequested() {
+            return mNetworkStarted.get();
+        }
+
+        public ConditionVariable getNetworkStartedCV() {
+            mNetworkStartedCV.close();
+            return mNetworkStartedCV;
+        }
+
+        public ConditionVariable getNetworkStoppedCV() {
+            mNetworkStoppedCV.close();
+            return mNetworkStoppedCV;
+        }
+
+        @Override
+        protected void handleAddRequest(NetworkRequest request, int score) {
+            // If we're expecting anything, we must be expecting additions.
+            if (mExpectations != null && !mExpectingAdditions) {
+                fail("Can't add requests while expecting requests to be removed");
+            }
+
+            // Add the request.
+            super.handleAddRequest(request, score);
+
+            // Reduce the number of request additions we're waiting for.
+            if (mExpectingAdditions) {
+                assertTrue("Added more requests than expected", mExpectations.getCount() > 0);
+                mExpectations.countDown();
+            }
+        }
+
+        @Override
+        protected void handleRemoveRequest(NetworkRequest request) {
+            // If we're expecting anything, we must be expecting removals.
+            if (mExpectations != null && mExpectingAdditions) {
+                fail("Can't remove requests while expecting requests to be added");
+            }
+
+            // Remove the request.
+            super.handleRemoveRequest(request);
+
+            // Reduce the number of request removals we're waiting for.
+            if (!mExpectingAdditions) {
+                assertTrue("Removed more requests than expected", mExpectations.getCount() > 0);
+                mExpectations.countDown();
+            }
+        }
+
+        private void assertNoExpectations() {
+            if (mExpectations != null) {
+                fail("Can't add expectation, " + mExpectations.getCount() + " already pending");
+            }
+        }
+
+        // Expects that count requests will be added.
+        public void expectAddRequests(final int count) {
+            assertNoExpectations();
+            mExpectingAdditions = true;
+            mExpectations = new CountDownLatch(count);
+        }
+
+        // Expects that count requests will be removed.
+        public void expectRemoveRequests(final int count) {
+            assertNoExpectations();
+            mExpectingAdditions = false;
+            mExpectations = new CountDownLatch(count);
+        }
+
+        // Waits for the expected request additions or removals to happen within a timeout.
+        public void waitForRequests() throws InterruptedException {
+            assertNotNull("Nothing to wait for", mExpectations);
+            mExpectations.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            final long count = mExpectations.getCount();
+            final String msg = count + " requests still not " +
+                    (mExpectingAdditions ? "added" : "removed") +
+                    " after " + TIMEOUT_MS + " ms";
+            assertEquals(msg, 0, count);
+            mExpectations = null;
+        }
+
+        public void waitForNetworkRequests(final int count) throws InterruptedException {
+            waitForRequests();
+            assertEquals(count, getMyRequestCount());
+        }
+    }
+
+    private class FakeWakeupMessage extends WakeupMessage {
+        private static final int UNREASONABLY_LONG_WAIT = 1000;
+
+        public FakeWakeupMessage(Context context, Handler handler, String cmdName, int cmd) {
+            super(context, handler, cmdName, cmd);
+        }
+
+        public FakeWakeupMessage(Context context, Handler handler, String cmdName, int cmd,
+                int arg1, int arg2, Object obj) {
+            super(context, handler, cmdName, cmd, arg1, arg2, obj);
+        }
+
+        @Override
+        public void schedule(long when) {
+            long delayMs = when - SystemClock.elapsedRealtime();
+            if (delayMs < 0) delayMs = 0;
+            if (delayMs > UNREASONABLY_LONG_WAIT) {
+                fail("Attempting to send msg more than " + UNREASONABLY_LONG_WAIT +
+                        "ms into the future: " + delayMs);
+            }
+            Message msg = mHandler.obtainMessage(mCmd, mArg1, mArg2, mObj);
+            mHandler.sendMessageDelayed(msg, delayMs);
+        }
+
+        @Override
+        public void cancel() {
+            mHandler.removeMessages(mCmd, mObj);
+        }
+
+        @Override
+        public void onAlarm() {
+            throw new AssertionError("Should never happen. Update this fake.");
+        }
+    }
+
+    // NetworkMonitor implementation allowing overriding of Internet connectivity probe result.
+    private class WrappedNetworkMonitor extends NetworkMonitor {
+        // HTTP response code fed back to NetworkMonitor for Internet connectivity probe.
+        public int gen204ProbeResult = 500;
+        public String gen204ProbeRedirectUrl = null;
+
+        public WrappedNetworkMonitor(Context context, Handler handler,
+                NetworkAgentInfo networkAgentInfo, NetworkRequest defaultRequest,
+                IpConnectivityLog log) {
+            super(context, handler, networkAgentInfo, defaultRequest, log);
+        }
+
+        @Override
+        protected CaptivePortalProbeResult isCaptivePortal() {
+            if (!mIsCaptivePortalCheckEnabled) { return new CaptivePortalProbeResult(204); }
+            return new CaptivePortalProbeResult(gen204ProbeResult, gen204ProbeRedirectUrl, null);
+        }
+    }
+
+    private class WrappedAvoidBadWifiTracker extends AvoidBadWifiTracker {
+        public volatile boolean configRestrictsAvoidBadWifi;
+
+        public WrappedAvoidBadWifiTracker(Context c, Handler h, Runnable r) {
+            super(c, h, r);
+        }
+
+        @Override
+        public boolean configRestrictsAvoidBadWifi() {
+            return configRestrictsAvoidBadWifi;
+        }
+    }
+
+    private class WrappedConnectivityService extends ConnectivityService {
+        public WrappedAvoidBadWifiTracker wrappedAvoidBadWifiTracker;
+        private WrappedNetworkMonitor mLastCreatedNetworkMonitor;
+
+        public WrappedConnectivityService(Context context, INetworkManagementService netManager,
+                INetworkStatsService statsService, INetworkPolicyManager policyManager,
+                IpConnectivityLog log) {
+            super(context, netManager, statsService, policyManager, log);
+            mLingerDelayMs = TEST_LINGER_DELAY_MS;
+        }
+
+        @Override
+        protected HandlerThread createHandlerThread() {
+            return new IdleableHandlerThread("WrappedConnectivityService");
+        }
+
+        @Override
+        protected int getDefaultTcpRwnd() {
+            // Prevent wrapped ConnectivityService from trying to write to SystemProperties.
+            return 0;
+        }
+
+        @Override
+        protected int reserveNetId() {
+            while (true) {
+                final int netId = super.reserveNetId();
+
+                // Don't overlap test NetIDs with real NetIDs as binding sockets to real networks
+                // can have odd side-effects, like network validations succeeding.
+                final Network[] networks = ConnectivityManager.from(getContext()).getAllNetworks();
+                boolean overlaps = false;
+                for (Network network : networks) {
+                    if (netId == network.netId) {
+                        overlaps = true;
+                        break;
+                    }
+                }
+                if (overlaps) continue;
+
+                return netId;
+            }
+        }
+
+        @Override
+        public NetworkMonitor createNetworkMonitor(Context context, Handler handler,
+                NetworkAgentInfo nai, NetworkRequest defaultRequest) {
+            final WrappedNetworkMonitor monitor = new WrappedNetworkMonitor(
+                    context, handler, nai, defaultRequest, mock(IpConnectivityLog.class));
+            mLastCreatedNetworkMonitor = monitor;
+            return monitor;
+        }
+
+        @Override
+        public AvoidBadWifiTracker createAvoidBadWifiTracker(
+                Context c, Handler h, Runnable r) {
+            final WrappedAvoidBadWifiTracker tracker = new WrappedAvoidBadWifiTracker(c, h, r);
+            return tracker;
+        }
+
+        public WrappedAvoidBadWifiTracker getAvoidBadWifiTracker() {
+            return (WrappedAvoidBadWifiTracker) mAvoidBadWifiTracker;
+        }
+
+        @Override
+        public WakeupMessage makeWakeupMessage(
+                Context context, Handler handler, String cmdName, int cmd, Object obj) {
+            return new FakeWakeupMessage(context, handler, cmdName, cmd, 0, 0, obj);
+        }
+
+        public WrappedNetworkMonitor getLastCreatedWrappedNetworkMonitor() {
+            return mLastCreatedNetworkMonitor;
+        }
+
+        public void waitForIdle(int timeoutMs) {
+            ((IdleableHandlerThread) mHandlerThread).waitForIdle(timeoutMs);
+        }
+
+        public void waitForIdle() {
+            waitForIdle(TIMEOUT_MS);
+        }
+    }
+
+    private interface Criteria {
+        public boolean get();
+    }
+
+    /**
+     * Wait up to 500ms for {@code criteria.get()} to become true, polling.
+     * Fails if 500ms goes by before {@code criteria.get()} to become true.
+     */
+    static private void waitFor(Criteria criteria) {
+        int delays = 0;
+        while (!criteria.get()) {
+            sleepFor(50);
+            if (++delays == 10) fail();
+        }
+    }
+
+    /**
+     * Wait up to TIMEOUT_MS for {@code conditionVariable} to open.
+     * Fails if TIMEOUT_MS goes by before {@code conditionVariable} opens.
+     */
+    static private void waitFor(ConditionVariable conditionVariable) {
+        assertTrue(conditionVariable.block(TIMEOUT_MS));
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        // InstrumentationTestRunner prepares a looper, but AndroidJUnitRunner does not.
+        // http://b/25897652 .
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        mServiceContext = new MockContext(getContext());
+        mService = new WrappedConnectivityService(mServiceContext,
+                mock(INetworkManagementService.class),
+                mock(INetworkStatsService.class),
+                mock(INetworkPolicyManager.class),
+                mock(IpConnectivityLog.class));
+
+        mService.systemReady();
+        mCm = new WrappedConnectivityManager(getContext(), mService);
+        mCm.bindProcessToNetwork(null);
+
+        // Ensure that the default setting for Captive Portals is used for most tests
+        setCaptivePortalMode(Settings.Global.CAPTIVE_PORTAL_MODE_PROMPT);
+    }
+
+    public void tearDown() throws Exception {
+        setMobileDataAlwaysOn(false);
+        if (mCellNetworkAgent != null) { mCellNetworkAgent.disconnect(); }
+        if (mWiFiNetworkAgent != null) { mWiFiNetworkAgent.disconnect(); }
+        mCellNetworkAgent = mWiFiNetworkAgent = null;
+        super.tearDown();
+    }
+
+    private int transportToLegacyType(int transport) {
+        switch (transport) {
+            case TRANSPORT_ETHERNET:
+                return TYPE_ETHERNET;
+            case TRANSPORT_WIFI:
+                return TYPE_WIFI;
+            case TRANSPORT_CELLULAR:
+                return TYPE_MOBILE;
+            default:
+                throw new IllegalStateException("Unknown transport " + transport);
+        }
+    }
+
+    private void verifyActiveNetwork(int transport) {
+        // Test getActiveNetworkInfo()
+        assertNotNull(mCm.getActiveNetworkInfo());
+        assertEquals(transportToLegacyType(transport), mCm.getActiveNetworkInfo().getType());
+        // Test getActiveNetwork()
+        assertNotNull(mCm.getActiveNetwork());
+        assertEquals(mCm.getActiveNetwork(), mCm.getActiveNetworkForUid(Process.myUid()));
+        switch (transport) {
+            case TRANSPORT_WIFI:
+                assertEquals(mCm.getActiveNetwork(), mWiFiNetworkAgent.getNetwork());
+                break;
+            case TRANSPORT_CELLULAR:
+                assertEquals(mCm.getActiveNetwork(), mCellNetworkAgent.getNetwork());
+                break;
+            default:
+                throw new IllegalStateException("Unknown transport" + transport);
+        }
+        // Test getNetworkInfo(Network)
+        assertNotNull(mCm.getNetworkInfo(mCm.getActiveNetwork()));
+        assertEquals(transportToLegacyType(transport), mCm.getNetworkInfo(mCm.getActiveNetwork()).getType());
+        // Test getNetworkCapabilities(Network)
+        assertNotNull(mCm.getNetworkCapabilities(mCm.getActiveNetwork()));
+        assertTrue(mCm.getNetworkCapabilities(mCm.getActiveNetwork()).hasTransport(transport));
+    }
+
+    private void verifyNoNetwork() {
+        // Test getActiveNetworkInfo()
+        assertNull(mCm.getActiveNetworkInfo());
+        // Test getActiveNetwork()
+        assertNull(mCm.getActiveNetwork());
+        assertNull(mCm.getActiveNetworkForUid(Process.myUid()));
+        // Test getAllNetworks()
+        assertEquals(0, mCm.getAllNetworks().length);
+    }
+
+    /**
+     * Return a ConditionVariable that opens when {@code count} numbers of CONNECTIVITY_ACTION
+     * broadcasts are received.
+     */
+    private ConditionVariable waitForConnectivityBroadcasts(final int count) {
+        final ConditionVariable cv = new ConditionVariable();
+        mServiceContext.registerReceiver(new BroadcastReceiver() {
+                    private int remaining = count;
+                    public void onReceive(Context context, Intent intent) {
+                        if (--remaining == 0) {
+                            cv.open();
+                            mServiceContext.unregisterReceiver(this);
+                        }
+                    }
+                }, new IntentFilter(CONNECTIVITY_ACTION));
+        return cv;
+    }
+
+    @SmallTest
+    public void testLingering() throws Exception {
+        verifyNoNetwork();
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        assertNull(mCm.getActiveNetworkInfo());
+        assertNull(mCm.getActiveNetwork());
+        // Test bringing up validated cellular.
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mCellNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        assertEquals(2, mCm.getAllNetworks().length);
+        assertTrue(mCm.getAllNetworks()[0].equals(mCm.getActiveNetwork()) ||
+                mCm.getAllNetworks()[1].equals(mCm.getActiveNetwork()));
+        assertTrue(mCm.getAllNetworks()[0].equals(mWiFiNetworkAgent.getNetwork()) ||
+                mCm.getAllNetworks()[1].equals(mWiFiNetworkAgent.getNetwork()));
+        // Test bringing up validated WiFi.
+        cv = waitForConnectivityBroadcasts(2);
+        mWiFiNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        assertEquals(2, mCm.getAllNetworks().length);
+        assertTrue(mCm.getAllNetworks()[0].equals(mCm.getActiveNetwork()) ||
+                mCm.getAllNetworks()[1].equals(mCm.getActiveNetwork()));
+        assertTrue(mCm.getAllNetworks()[0].equals(mCellNetworkAgent.getNetwork()) ||
+                mCm.getAllNetworks()[1].equals(mCellNetworkAgent.getNetwork()));
+        // Test cellular linger timeout.
+        waitFor(new Criteria() {
+                public boolean get() { return mCm.getAllNetworks().length == 1; } });
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        assertEquals(1, mCm.getAllNetworks().length);
+        assertEquals(mCm.getAllNetworks()[0], mCm.getActiveNetwork());
+        // Test WiFi disconnect.
+        cv = waitForConnectivityBroadcasts(1);
+        mWiFiNetworkAgent.disconnect();
+        waitFor(cv);
+        verifyNoNetwork();
+    }
+
+    @SmallTest
+    public void testValidatedCellularOutscoresUnvalidatedWiFi() throws Exception {
+        // Test bringing up unvalidated WiFi
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mWiFiNetworkAgent.connect(false);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test bringing up unvalidated cellular
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(false);
+        mService.waitForIdle();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test cellular disconnect.
+        mCellNetworkAgent.disconnect();
+        mService.waitForIdle();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test bringing up validated cellular
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        cv = waitForConnectivityBroadcasts(2);
+        mCellNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test cellular disconnect.
+        cv = waitForConnectivityBroadcasts(2);
+        mCellNetworkAgent.disconnect();
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test WiFi disconnect.
+        cv = waitForConnectivityBroadcasts(1);
+        mWiFiNetworkAgent.disconnect();
+        waitFor(cv);
+        verifyNoNetwork();
+    }
+
+    @SmallTest
+    public void testUnvalidatedWifiOutscoresUnvalidatedCellular() throws Exception {
+        // Test bringing up unvalidated cellular.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mCellNetworkAgent.connect(false);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test bringing up unvalidated WiFi.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        cv = waitForConnectivityBroadcasts(2);
+        mWiFiNetworkAgent.connect(false);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test WiFi disconnect.
+        cv = waitForConnectivityBroadcasts(2);
+        mWiFiNetworkAgent.disconnect();
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test cellular disconnect.
+        cv = waitForConnectivityBroadcasts(1);
+        mCellNetworkAgent.disconnect();
+        waitFor(cv);
+        verifyNoNetwork();
+    }
+
+    @SmallTest
+    public void testUnlingeringDoesNotValidate() throws Exception {
+        // Test bringing up unvalidated WiFi.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mWiFiNetworkAgent.connect(false);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        // Test bringing up validated cellular.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        cv = waitForConnectivityBroadcasts(2);
+        mCellNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        // Test cellular disconnect.
+        cv = waitForConnectivityBroadcasts(2);
+        mCellNetworkAgent.disconnect();
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Unlingering a network should not cause it to be marked as validated.
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+    }
+
+    @SmallTest
+    public void testCellularOutscoresWeakWifi() throws Exception {
+        // Test bringing up validated cellular.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mCellNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test bringing up validated WiFi.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        cv = waitForConnectivityBroadcasts(2);
+        mWiFiNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test WiFi getting really weak.
+        cv = waitForConnectivityBroadcasts(2);
+        mWiFiNetworkAgent.adjustScore(-11);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test WiFi restoring signal strength.
+        cv = waitForConnectivityBroadcasts(2);
+        mWiFiNetworkAgent.adjustScore(11);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+    }
+
+    @SmallTest
+    public void testReapingNetwork() throws Exception {
+        // Test bringing up WiFi without NET_CAPABILITY_INTERNET.
+        // Expect it to be torn down immediately because it satisfies no requests.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        ConditionVariable cv = mWiFiNetworkAgent.getDisconnectedCV();
+        mWiFiNetworkAgent.connectWithoutInternet();
+        waitFor(cv);
+        // Test bringing up cellular without NET_CAPABILITY_INTERNET.
+        // Expect it to be torn down immediately because it satisfies no requests.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        cv = mCellNetworkAgent.getDisconnectedCV();
+        mCellNetworkAgent.connectWithoutInternet();
+        waitFor(cv);
+        // Test bringing up validated WiFi.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        cv = waitForConnectivityBroadcasts(1);
+        mWiFiNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test bringing up unvalidated cellular.
+        // Expect it to be torn down because it could never be the highest scoring network
+        // satisfying the default request even if it validated.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        cv = mCellNetworkAgent.getDisconnectedCV();
+        mCellNetworkAgent.connect(false);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        cv = mWiFiNetworkAgent.getDisconnectedCV();
+        mWiFiNetworkAgent.disconnect();
+        waitFor(cv);
+    }
+
+    @SmallTest
+    public void testCellularFallback() throws Exception {
+        // Test bringing up validated cellular.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mCellNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test bringing up validated WiFi.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        cv = waitForConnectivityBroadcasts(2);
+        mWiFiNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Reevaluate WiFi (it'll instantly fail DNS).
+        cv = waitForConnectivityBroadcasts(2);
+        assertTrue(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        mCm.reportBadNetwork(mWiFiNetworkAgent.getNetwork());
+        // Should quickly fall back to Cellular.
+        waitFor(cv);
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Reevaluate cellular (it'll instantly fail DNS).
+        cv = waitForConnectivityBroadcasts(2);
+        assertTrue(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        mCm.reportBadNetwork(mCellNetworkAgent.getNetwork());
+        // Should quickly fall back to WiFi.
+        waitFor(cv);
+        assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        verifyActiveNetwork(TRANSPORT_WIFI);
+    }
+
+    @SmallTest
+    public void testWiFiFallback() throws Exception {
+        // Test bringing up unvalidated WiFi.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mWiFiNetworkAgent.connect(false);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test bringing up validated cellular.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        cv = waitForConnectivityBroadcasts(2);
+        mCellNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Reevaluate cellular (it'll instantly fail DNS).
+        cv = waitForConnectivityBroadcasts(2);
+        assertTrue(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        mCm.reportBadNetwork(mCellNetworkAgent.getNetwork());
+        // Should quickly fall back to WiFi.
+        waitFor(cv);
+        assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        verifyActiveNetwork(TRANSPORT_WIFI);
+    }
+
+    enum CallbackState {
+        NONE,
+        AVAILABLE,
+        NETWORK_CAPABILITIES,
+        LINK_PROPERTIES,
+        LOSING,
+        LOST,
+        UNAVAILABLE
+    }
+
+    private static class CallbackInfo {
+        public final CallbackState state;
+        public final Network network;
+        public final Object arg;
+        public CallbackInfo(CallbackState s, Network n, Object o) {
+            state = s; network = n; arg = o;
+        }
+        public String toString() {
+            return String.format("%s (%s)", state, network);
+        }
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof CallbackInfo)) return false;
+            // Ignore timeMs, since it's unpredictable.
+            CallbackInfo other = (CallbackInfo) o;
+            return (state == other.state) && Objects.equals(network, other.network);
+        }
+        @Override
+        public int hashCode() {
+            return Objects.hash(state, network);
+        }
+    }
+
+    /**
+     * Utility NetworkCallback for testing. The caller must explicitly test for all the callbacks
+     * this class receives, by calling expectCallback() exactly once each time a callback is
+     * received. assertNoCallback may be called at any time.
+     */
+    private class TestNetworkCallback extends NetworkCallback {
+        // Chosen to be much less than the linger timeout. This ensures that we can distinguish
+        // between a LOST callback that arrives immediately and a LOST callback that arrives after
+        // the linger timeout.
+        private final static int TIMEOUT_MS = 50;
+
+        private final LinkedBlockingQueue<CallbackInfo> mCallbacks = new LinkedBlockingQueue<>();
+
+        protected void setLastCallback(CallbackState state, Network network, Object o) {
+            mCallbacks.offer(new CallbackInfo(state, network, o));
+        }
+
+        @Override
+        public void onAvailable(Network network) {
+            setLastCallback(CallbackState.AVAILABLE, network, null);
+        }
+
+        @Override
+        public void onUnavailable() {
+            setLastCallback(CallbackState.UNAVAILABLE, null, null);
+        }
+
+        @Override
+        public void onLosing(Network network, int maxMsToLive) {
+            setLastCallback(CallbackState.LOSING, network, maxMsToLive /* autoboxed int */);
+        }
+
+        @Override
+        public void onLost(Network network) {
+            setLastCallback(CallbackState.LOST, network, null);
+        }
+
+        CallbackInfo nextCallback(int timeoutMs) {
+            CallbackInfo cb = null;
+            try {
+                cb = mCallbacks.poll(timeoutMs, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+            }
+            if (cb == null) {
+                // LinkedBlockingQueue.poll() returns null if it timeouts.
+                fail("Did not receive callback after " + timeoutMs + "ms");
+            }
+            return cb;
+        }
+
+        void expectCallback(CallbackState state, MockNetworkAgent mockAgent, int timeoutMs) {
+            CallbackInfo expected = new CallbackInfo(
+                    state, (mockAgent != null) ? mockAgent.getNetwork() : null, 0);
+            CallbackInfo actual = nextCallback(timeoutMs);
+            assertEquals("Unexpected callback:", expected, actual);
+            if (state == CallbackState.LOSING) {
+                String msg = String.format(
+                        "Invalid linger time value %d, must be between %d and %d",
+                        actual.arg, 0, TEST_LINGER_DELAY_MS);
+                int maxMsToLive = (Integer) actual.arg;
+                assertTrue(msg, 0 <= maxMsToLive && maxMsToLive <= TEST_LINGER_DELAY_MS);
+            }
+        }
+
+        void expectCallback(CallbackState state, MockNetworkAgent mockAgent) {
+            expectCallback(state, mockAgent, TIMEOUT_MS);
+        }
+
+        void assertNoCallback() {
+            mService.waitForIdle();
+            CallbackInfo c = mCallbacks.peek();
+            assertNull("Unexpected callback: " + c, c);
+        }
+    }
+
+    // Can't be part of TestNetworkCallback because "cannot be declared static; static methods can
+    // only be declared in a static or top level type".
+    static void assertNoCallbacks(TestNetworkCallback ... callbacks) {
+        for (TestNetworkCallback c : callbacks) {
+            c.assertNoCallback();
+        }
+    }
+
+    @SmallTest
+    public void testStateChangeNetworkCallbacks() throws Exception {
+        final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback();
+        final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback();
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest genericRequest = new NetworkRequest.Builder()
+                .clearCapabilities().build();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.registerNetworkCallback(genericRequest, genericNetworkCallback);
+        mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
+        mCm.registerNetworkCallback(cellRequest, cellNetworkCallback);
+
+        // Test unvalidated networks
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(false);
+        genericNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        waitFor(cv);
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+        // This should not trigger spurious onAvailable() callbacks, b/21762680.
+        mCellNetworkAgent.adjustScore(-1);
+        mService.waitForIdle();
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        cv = waitForConnectivityBroadcasts(2);
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        genericNetworkCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        wifiNetworkCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        waitFor(cv);
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+        cv = waitForConnectivityBroadcasts(2);
+        mWiFiNetworkAgent.disconnect();
+        genericNetworkCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        wifiNetworkCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+        waitFor(cv);
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+        cv = waitForConnectivityBroadcasts(1);
+        mCellNetworkAgent.disconnect();
+        genericNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+        cellNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+        waitFor(cv);
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+        // Test validated networks
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        genericNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+        // This should not trigger spurious onAvailable() callbacks, b/21762680.
+        mCellNetworkAgent.adjustScore(-1);
+        mService.waitForIdle();
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        genericNetworkCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        genericNetworkCallback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+        wifiNetworkCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        cellNetworkCallback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+        mWiFiNetworkAgent.disconnect();
+        genericNetworkCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        wifiNetworkCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+        mCellNetworkAgent.disconnect();
+        genericNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+        cellNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+    }
+
+    @SmallTest
+    public void testMultipleLingering() {
+        NetworkRequest request = new NetworkRequest.Builder()
+                .clearCapabilities().addCapability(NET_CAPABILITY_NOT_METERED)
+                .build();
+        TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+
+        TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mEthernetNetworkAgent = new MockNetworkAgent(TRANSPORT_ETHERNET);
+
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        mEthernetNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+
+        mCellNetworkAgent.connect(true);
+        callback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        mWiFiNetworkAgent.connect(true);
+        // We get AVAILABLE on wifi when wifi connects and satisfies our unmetered request.
+        // We then get LOSING when wifi validates and cell is outscored.
+        callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        mEthernetNetworkAgent.connect(true);
+        callback.expectCallback(CallbackState.AVAILABLE, mEthernetNetworkAgent);
+        callback.expectCallback(CallbackState.LOSING, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mEthernetNetworkAgent);
+        assertEquals(mEthernetNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        mEthernetNetworkAgent.disconnect();
+        callback.expectCallback(CallbackState.LOST, mEthernetNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.LOST, mEthernetNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+
+        for (int i = 0; i < 4; i++) {
+            MockNetworkAgent oldNetwork, newNetwork;
+            if (i % 2 == 0) {
+                mWiFiNetworkAgent.adjustScore(-15);
+                oldNetwork = mWiFiNetworkAgent;
+                newNetwork = mCellNetworkAgent;
+            } else {
+                mWiFiNetworkAgent.adjustScore(15);
+                oldNetwork = mCellNetworkAgent;
+                newNetwork = mWiFiNetworkAgent;
+
+            }
+            callback.expectCallback(CallbackState.LOSING, oldNetwork);
+            // TODO: should we send an AVAILABLE callback to newNetwork, to indicate that it is no
+            // longer lingering?
+            defaultCallback.expectCallback(CallbackState.AVAILABLE, newNetwork);
+            assertEquals(newNetwork.getNetwork(), mCm.getActiveNetwork());
+        }
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // Verify that if a network no longer satisfies a request, we send LOST and not LOSING, even
+        // if the network is still up.
+        mWiFiNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED);
+        callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+
+        // Wifi no longer satisfies our listen, which is for an unmetered network.
+        // But because its score is 55, it's still up (and the default network).
+        defaultCallback.assertNoCallback();
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // Disconnect our test networks.
+        mWiFiNetworkAgent.disconnect();
+        defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        mCellNetworkAgent.disconnect();
+        defaultCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+
+        mCm.unregisterNetworkCallback(callback);
+        mService.waitForIdle();
+
+        // Check that a network is only lingered or torn down if it would not satisfy a request even
+        // if it validated.
+        request = new NetworkRequest.Builder().clearCapabilities().build();
+        callback = new TestNetworkCallback();
+
+        mCm.registerNetworkCallback(request, callback);
+
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(false);   // Score: 10
+        callback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // Bring up wifi with a score of 20.
+        // Cell stays up because it would satisfy the default request if it validated.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);   // Score: 20
+        callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // Bring up wifi with a score of 70.
+        // Cell is lingered because it would not satisfy any request, even if it validated.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.adjustScore(50);
+        mWiFiNetworkAgent.connect(false);   // Score: 70
+        callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // Tear down wifi.
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // Bring up wifi, then validate it. Previous versions would immediately tear down cell, but
+        // it's arguably correct to linger it, since it was the default network before it validated.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        mCellNetworkAgent.disconnect();
+        callback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+
+        // If a network is lingering, and we add and remove a request from it, resume lingering.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        callback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+
+        NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        NetworkCallback noopCallback = new NetworkCallback();
+        mCm.requestNetwork(cellRequest, noopCallback);
+        // TODO: should this cause an AVAILABLE callback, to indicate that the network is no longer
+        // lingering?
+        mCm.unregisterNetworkCallback(noopCallback);
+        callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+
+        // Similar to the above: lingering can start even after the lingered request is removed.
+        // Disconnect wifi and switch to cell.
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+
+        // Cell is now the default network. Pin it with a cell-specific request.
+        noopCallback = new NetworkCallback();  // Can't reuse NetworkCallbacks. http://b/20701525
+        mCm.requestNetwork(cellRequest, noopCallback);
+
+        // Now connect wifi, and expect it to become the default network.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        // The default request is lingering on cell, but nothing happens to cell, and we send no
+        // callbacks for it, because it's kept up by cellRequest.
+        callback.assertNoCallback();
+        // Now unregister cellRequest and expect cell to start lingering.
+        mCm.unregisterNetworkCallback(noopCallback);
+        callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+
+        // Let linger run its course.
+        callback.assertNoCallback();
+        callback.expectCallback(CallbackState.LOST, mCellNetworkAgent,
+                TEST_LINGER_DELAY_MS /* timeoutMs */);
+
+        // Clean up.
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+
+        mCm.unregisterNetworkCallback(callback);
+        mCm.unregisterNetworkCallback(defaultCallback);
+    }
+
+    private void tryNetworkFactoryRequests(int capability) throws Exception {
+        // Verify NOT_RESTRICTED is set appropriately
+        final NetworkCapabilities nc = new NetworkRequest.Builder().addCapability(capability)
+                .build().networkCapabilities;
+        if (capability == NET_CAPABILITY_CBS || capability == NET_CAPABILITY_DUN ||
+                capability == NET_CAPABILITY_EIMS || capability == NET_CAPABILITY_FOTA ||
+                capability == NET_CAPABILITY_IA || capability == NET_CAPABILITY_IMS ||
+                capability == NET_CAPABILITY_RCS || capability == NET_CAPABILITY_XCAP) {
+            assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        } else {
+            assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        }
+
+        NetworkCapabilities filter = new NetworkCapabilities();
+        filter.addCapability(capability);
+        final HandlerThread handlerThread = new HandlerThread("testNetworkFactoryRequests");
+        handlerThread.start();
+        final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "testFactory", filter);
+        testFactory.setScoreFilter(40);
+        ConditionVariable cv = testFactory.getNetworkStartedCV();
+        testFactory.expectAddRequests(1);
+        testFactory.register();
+        testFactory.waitForNetworkRequests(1);
+        int expectedRequestCount = 1;
+        NetworkCallback networkCallback = null;
+        // For non-INTERNET capabilities we cannot rely on the default request being present, so
+        // add one.
+        if (capability != NET_CAPABILITY_INTERNET) {
+            assertFalse(testFactory.getMyStartRequested());
+            NetworkRequest request = new NetworkRequest.Builder().addCapability(capability).build();
+            networkCallback = new NetworkCallback();
+            testFactory.expectAddRequests(1);
+            mCm.requestNetwork(request, networkCallback);
+            expectedRequestCount++;
+            testFactory.waitForNetworkRequests(expectedRequestCount);
+        }
+        waitFor(cv);
+        assertEquals(expectedRequestCount, testFactory.getMyRequestCount());
+        assertTrue(testFactory.getMyStartRequested());
+
+        // Now bring in a higher scored network.
+        MockNetworkAgent testAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        // Rather than create a validated network which complicates things by registering it's
+        // own NetworkRequest during startup, just bump up the score to cancel out the
+        // unvalidated penalty.
+        testAgent.adjustScore(40);
+        cv = testFactory.getNetworkStoppedCV();
+
+        // When testAgent connects, ConnectivityService will re-send us all current requests with
+        // the new score. There are expectedRequestCount such requests, and we must wait for all of
+        // them.
+        testFactory.expectAddRequests(expectedRequestCount);
+        testAgent.connect(false);
+        testAgent.addCapability(capability);
+        waitFor(cv);
+        testFactory.waitForNetworkRequests(expectedRequestCount);
+        assertFalse(testFactory.getMyStartRequested());
+
+        // Bring in a bunch of requests.
+        testFactory.expectAddRequests(10);
+        assertEquals(expectedRequestCount, testFactory.getMyRequestCount());
+        ConnectivityManager.NetworkCallback[] networkCallbacks =
+                new ConnectivityManager.NetworkCallback[10];
+        for (int i = 0; i< networkCallbacks.length; i++) {
+            networkCallbacks[i] = new ConnectivityManager.NetworkCallback();
+            NetworkRequest.Builder builder = new NetworkRequest.Builder();
+            builder.addCapability(capability);
+            mCm.requestNetwork(builder.build(), networkCallbacks[i]);
+        }
+        testFactory.waitForNetworkRequests(10 + expectedRequestCount);
+        assertFalse(testFactory.getMyStartRequested());
+
+        // Remove the requests.
+        testFactory.expectRemoveRequests(10);
+        for (int i = 0; i < networkCallbacks.length; i++) {
+            mCm.unregisterNetworkCallback(networkCallbacks[i]);
+        }
+        testFactory.waitForNetworkRequests(expectedRequestCount);
+        assertFalse(testFactory.getMyStartRequested());
+
+        // Drop the higher scored network.
+        cv = testFactory.getNetworkStartedCV();
+        testAgent.disconnect();
+        waitFor(cv);
+        assertEquals(expectedRequestCount, testFactory.getMyRequestCount());
+        assertTrue(testFactory.getMyStartRequested());
+
+        testFactory.unregister();
+        if (networkCallback != null) mCm.unregisterNetworkCallback(networkCallback);
+        handlerThread.quit();
+    }
+
+    @SmallTest
+    public void testNetworkFactoryRequests() throws Exception {
+        tryNetworkFactoryRequests(NET_CAPABILITY_MMS);
+        tryNetworkFactoryRequests(NET_CAPABILITY_SUPL);
+        tryNetworkFactoryRequests(NET_CAPABILITY_DUN);
+        tryNetworkFactoryRequests(NET_CAPABILITY_FOTA);
+        tryNetworkFactoryRequests(NET_CAPABILITY_IMS);
+        tryNetworkFactoryRequests(NET_CAPABILITY_CBS);
+        tryNetworkFactoryRequests(NET_CAPABILITY_WIFI_P2P);
+        tryNetworkFactoryRequests(NET_CAPABILITY_IA);
+        tryNetworkFactoryRequests(NET_CAPABILITY_RCS);
+        tryNetworkFactoryRequests(NET_CAPABILITY_XCAP);
+        tryNetworkFactoryRequests(NET_CAPABILITY_EIMS);
+        tryNetworkFactoryRequests(NET_CAPABILITY_NOT_METERED);
+        tryNetworkFactoryRequests(NET_CAPABILITY_INTERNET);
+        tryNetworkFactoryRequests(NET_CAPABILITY_TRUSTED);
+        tryNetworkFactoryRequests(NET_CAPABILITY_NOT_VPN);
+        // Skipping VALIDATED and CAPTIVE_PORTAL as they're disallowed.
+    }
+
+    @SmallTest
+    public void testNoMutableNetworkRequests() throws Exception {
+        PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, new Intent("a"), 0);
+        NetworkRequest.Builder builder = new NetworkRequest.Builder();
+        builder.addCapability(NET_CAPABILITY_VALIDATED);
+        try {
+            mCm.requestNetwork(builder.build(), new NetworkCallback());
+            fail();
+        } catch (IllegalArgumentException expected) {}
+        try {
+            mCm.requestNetwork(builder.build(), pendingIntent);
+            fail();
+        } catch (IllegalArgumentException expected) {}
+        builder = new NetworkRequest.Builder();
+        builder.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
+        try {
+            mCm.requestNetwork(builder.build(), new NetworkCallback());
+            fail();
+        } catch (IllegalArgumentException expected) {}
+        try {
+            mCm.requestNetwork(builder.build(), pendingIntent);
+            fail();
+        } catch (IllegalArgumentException expected) {}
+    }
+
+    @SmallTest
+    public void testMMSonWiFi() throws Exception {
+        // Test bringing up cellular without MMS NetworkRequest gets reaped
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_MMS);
+        ConditionVariable cv = mCellNetworkAgent.getDisconnectedCV();
+        mCellNetworkAgent.connectWithoutInternet();
+        waitFor(cv);
+        waitFor(new Criteria() {
+                public boolean get() { return mCm.getAllNetworks().length == 0; } });
+        verifyNoNetwork();
+        // Test bringing up validated WiFi.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        cv = waitForConnectivityBroadcasts(1);
+        mWiFiNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Register MMS NetworkRequest
+        NetworkRequest.Builder builder = new NetworkRequest.Builder();
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS);
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.requestNetwork(builder.build(), networkCallback);
+        // Test bringing up unvalidated cellular with MMS
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_MMS);
+        mCellNetworkAgent.connectWithoutInternet();
+        networkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test releasing NetworkRequest disconnects cellular with MMS
+        cv = mCellNetworkAgent.getDisconnectedCV();
+        mCm.unregisterNetworkCallback(networkCallback);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+    }
+
+    @SmallTest
+    public void testMMSonCell() throws Exception {
+        // Test bringing up cellular without MMS
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mCellNetworkAgent.connect(false);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Register MMS NetworkRequest
+        NetworkRequest.Builder builder = new NetworkRequest.Builder();
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS);
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.requestNetwork(builder.build(), networkCallback);
+        // Test bringing up MMS cellular network
+        MockNetworkAgent mmsNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mmsNetworkAgent.addCapability(NET_CAPABILITY_MMS);
+        mmsNetworkAgent.connectWithoutInternet();
+        networkCallback.expectCallback(CallbackState.AVAILABLE, mmsNetworkAgent);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test releasing MMS NetworkRequest does not disconnect main cellular NetworkAgent
+        cv = mmsNetworkAgent.getDisconnectedCV();
+        mCm.unregisterNetworkCallback(networkCallback);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+    }
+
+    @SmallTest
+    public void testCaptivePortal() {
+        final TestNetworkCallback captivePortalCallback = new TestNetworkCallback();
+        final NetworkRequest captivePortalRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build();
+        mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback);
+
+        final TestNetworkCallback validatedCallback = new TestNetworkCallback();
+        final NetworkRequest validatedRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_VALIDATED).build();
+        mCm.registerNetworkCallback(validatedRequest, validatedCallback);
+
+        // Bring up a network with a captive portal.
+        // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        String firstRedirectUrl = "http://example.com/firstPath";
+        mWiFiNetworkAgent.connectWithCaptivePortal(firstRedirectUrl);
+        captivePortalCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), firstRedirectUrl);
+
+        // Take down network.
+        // Expect onLost callback.
+        mWiFiNetworkAgent.disconnect();
+        captivePortalCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+
+        // Bring up a network with a captive portal.
+        // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        String secondRedirectUrl = "http://example.com/secondPath";
+        mWiFiNetworkAgent.connectWithCaptivePortal(secondRedirectUrl);
+        captivePortalCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), secondRedirectUrl);
+
+        // Make captive portal disappear then revalidate.
+        // Expect onLost callback because network no longer provides NET_CAPABILITY_CAPTIVE_PORTAL.
+        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 204;
+        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true);
+        captivePortalCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+
+        // Expect NET_CAPABILITY_VALIDATED onAvailable callback.
+        validatedCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+
+        // Break network connectivity.
+        // Expect NET_CAPABILITY_VALIDATED onLost callback.
+        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 500;
+        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), false);
+        validatedCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+    }
+
+    @SmallTest
+    public void testAvoidOrIgnoreCaptivePortals() {
+        final TestNetworkCallback captivePortalCallback = new TestNetworkCallback();
+        final NetworkRequest captivePortalRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build();
+        mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback);
+
+        final TestNetworkCallback validatedCallback = new TestNetworkCallback();
+        final NetworkRequest validatedRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_VALIDATED).build();
+        mCm.registerNetworkCallback(validatedRequest, validatedCallback);
+
+        setCaptivePortalMode(Settings.Global.CAPTIVE_PORTAL_MODE_AVOID);
+        // Bring up a network with a captive portal.
+        // Expect it to fail to connect and not result in any callbacks.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        String firstRedirectUrl = "http://example.com/firstPath";
+
+        ConditionVariable disconnectCv = mWiFiNetworkAgent.getDisconnectedCV();
+        ConditionVariable avoidCv = mWiFiNetworkAgent.getPreventReconnectReceived();
+        mWiFiNetworkAgent.connectWithCaptivePortal(firstRedirectUrl);
+        waitFor(disconnectCv);
+        waitFor(avoidCv);
+
+        assertNoCallbacks(captivePortalCallback, validatedCallback);
+
+        // Now test ignore mode.
+        setCaptivePortalMode(Settings.Global.CAPTIVE_PORTAL_MODE_IGNORE);
+
+        // Bring up a network with a captive portal.
+        // Since we're ignoring captive portals, the network will validate.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        String secondRedirectUrl = "http://example.com/secondPath";
+        mWiFiNetworkAgent.connectWithCaptivePortal(secondRedirectUrl);
+
+        // Expect NET_CAPABILITY_VALIDATED onAvailable callback.
+        validatedCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        // But there should be no CaptivePortal callback.
+        captivePortalCallback.assertNoCallback();
+    }
+
+    @SmallTest
+    public void testInvalidNetworkSpecifier() {
+        boolean execptionCalled = true;
+
+        try {
+            NetworkRequest.Builder builder = new NetworkRequest.Builder();
+            builder.setNetworkSpecifier(MATCH_ALL_REQUESTS_NETWORK_SPECIFIER);
+            execptionCalled = false;
+        } catch (IllegalArgumentException e) {
+            // do nothing - should get here
+        }
+
+        assertTrue("NetworkRequest builder with MATCH_ALL_REQUESTS_NETWORK_SPECIFIER",
+                execptionCalled);
+
+        try {
+            NetworkCapabilities networkCapabilities = new NetworkCapabilities();
+            networkCapabilities.addTransportType(TRANSPORT_WIFI)
+                    .setNetworkSpecifier(NetworkCapabilities.MATCH_ALL_REQUESTS_NETWORK_SPECIFIER);
+            mService.requestNetwork(networkCapabilities, null, 0, null,
+                    ConnectivityManager.TYPE_WIFI);
+            execptionCalled = false;
+        } catch (IllegalArgumentException e) {
+            // do nothing - should get here
+        }
+
+        assertTrue("ConnectivityService requestNetwork with MATCH_ALL_REQUESTS_NETWORK_SPECIFIER",
+                execptionCalled);
+    }
+
+    @SmallTest
+    public void testRegisterDefaultNetworkCallback() throws Exception {
+        final TestNetworkCallback defaultNetworkCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(defaultNetworkCallback);
+        defaultNetworkCallback.assertNoCallback();
+
+        // Create a TRANSPORT_CELLULAR request to keep the mobile interface up
+        // whenever Wi-Fi is up. Without this, the mobile network agent is
+        // reaped before any other activity can take place.
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.requestNetwork(cellRequest, cellNetworkCallback);
+        cellNetworkCallback.assertNoCallback();
+
+        // Bring up cell and expect CALLBACK_AVAILABLE.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        defaultNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+
+        // Bring up wifi and expect CALLBACK_AVAILABLE.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        cellNetworkCallback.assertNoCallback();
+        defaultNetworkCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+
+        // Bring down cell. Expect no default network callback, since it wasn't the default.
+        mCellNetworkAgent.disconnect();
+        cellNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+        defaultNetworkCallback.assertNoCallback();
+
+        // Bring up cell. Expect no default network callback, since it won't be the default.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        defaultNetworkCallback.assertNoCallback();
+
+        // Bring down wifi. Expect the default network callback to notified of LOST wifi
+        // followed by AVAILABLE cell.
+        mWiFiNetworkAgent.disconnect();
+        cellNetworkCallback.assertNoCallback();
+        defaultNetworkCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        mCellNetworkAgent.disconnect();
+        cellNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+        defaultNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+    }
+
+    private class TestRequestUpdateCallback extends TestNetworkCallback {
+        @Override
+        public void onCapabilitiesChanged(Network network, NetworkCapabilities netCap) {
+            setLastCallback(CallbackState.NETWORK_CAPABILITIES, network, netCap);
+        }
+
+        @Override
+        public void onLinkPropertiesChanged(Network network, LinkProperties linkProp) {
+            setLastCallback(CallbackState.LINK_PROPERTIES, network, linkProp);
+        }
+    }
+
+    @SmallTest
+    public void testRequestCallbackUpdates() throws Exception {
+        // File a network request for mobile.
+        final TestNetworkCallback cellNetworkCallback = new TestRequestUpdateCallback();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.requestNetwork(cellRequest, cellNetworkCallback);
+
+        // Bring up the mobile network.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        // We should get onAvailable().
+        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        // We should get onCapabilitiesChanged(), when the mobile network successfully validates.
+        cellNetworkCallback.expectCallback(CallbackState.NETWORK_CAPABILITIES, mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+
+        // Update LinkProperties.
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("foonet_data0");
+        mCellNetworkAgent.sendLinkProperties(lp);
+        // We should get onLinkPropertiesChanged().
+        cellNetworkCallback.expectCallback(CallbackState.LINK_PROPERTIES, mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+
+        // Register a garden variety default network request.
+        final TestNetworkCallback dfltNetworkCallback = new TestRequestUpdateCallback();
+        mCm.registerDefaultNetworkCallback(dfltNetworkCallback);
+        // Only onAvailable() is called; no other information is delivered.
+        dfltNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        dfltNetworkCallback.assertNoCallback();
+
+        // Request a NetworkCapabilities update; only the requesting callback is notified.
+        mCm.requestNetworkCapabilities(dfltNetworkCallback);
+        dfltNetworkCallback.expectCallback(CallbackState.NETWORK_CAPABILITIES, mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+        dfltNetworkCallback.assertNoCallback();
+
+        // Request a LinkProperties update; only the requesting callback is notified.
+        mCm.requestLinkProperties(dfltNetworkCallback);
+        dfltNetworkCallback.expectCallback(CallbackState.LINK_PROPERTIES, mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+        dfltNetworkCallback.assertNoCallback();
+
+        mCm.unregisterNetworkCallback(dfltNetworkCallback);
+        mCm.unregisterNetworkCallback(cellNetworkCallback);
+    }
+
+    private void setCaptivePortalMode(int mode) {
+        ContentResolver cr = mServiceContext.getContentResolver();
+        Settings.Global.putInt(cr, Settings.Global.CAPTIVE_PORTAL_MODE, mode);
+    }
+
+    private void setMobileDataAlwaysOn(boolean enable) {
+        ContentResolver cr = mServiceContext.getContentResolver();
+        Settings.Global.putInt(cr, Settings.Global.MOBILE_DATA_ALWAYS_ON, enable ? 1 : 0);
+        mService.updateMobileDataAlwaysOn();
+        mService.waitForIdle();
+    }
+
+    private boolean isForegroundNetwork(MockNetworkAgent network) {
+        NetworkCapabilities nc = mCm.getNetworkCapabilities(network.getNetwork());
+        assertNotNull(nc);
+        return nc.hasCapability(NET_CAPABILITY_FOREGROUND);
+    }
+
+    @SmallTest
+    public void testBackgroundNetworks() throws Exception {
+        // Create a background request. We can't do this ourselves because ConnectivityService
+        // doesn't have an API for it. So just turn on mobile data always on.
+        setMobileDataAlwaysOn(true);
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+        final NetworkRequest fgRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_FOREGROUND).build();
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        final TestNetworkCallback fgCallback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+        mCm.registerNetworkCallback(fgRequest, fgCallback);
+
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        callback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        fgCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertTrue(isForegroundNetwork(mCellNetworkAgent));
+
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+
+        // When wifi connects, cell lingers.
+        callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        fgCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+        fgCallback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+        assertTrue(isForegroundNetwork(mCellNetworkAgent));
+        assertTrue(isForegroundNetwork(mWiFiNetworkAgent));
+
+        // When lingering is complete, cell is still there but is now in the background.
+        fgCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent, TEST_LINGER_DELAY_MS);
+        callback.assertNoCallback();
+        assertFalse(isForegroundNetwork(mCellNetworkAgent));
+        assertTrue(isForegroundNetwork(mWiFiNetworkAgent));
+
+        // File a cell request and check that cell comes into the foreground.
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        final TestNetworkCallback cellCallback = new TestNetworkCallback();
+        mCm.requestNetwork(cellRequest, cellCallback);
+        cellCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        fgCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        callback.assertNoCallback();  // Because the network is already up.
+        assertTrue(isForegroundNetwork(mCellNetworkAgent));
+        assertTrue(isForegroundNetwork(mWiFiNetworkAgent));
+
+        // Release the request. The network immediately goes into the background, since it was not
+        // lingering.
+        mCm.unregisterNetworkCallback(cellCallback);
+        fgCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+        callback.assertNoCallback();
+        assertFalse(isForegroundNetwork(mCellNetworkAgent));
+        assertTrue(isForegroundNetwork(mWiFiNetworkAgent));
+
+        // Disconnect wifi and check that cell is foreground again.
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        fgCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        fgCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertTrue(isForegroundNetwork(mCellNetworkAgent));
+
+        mCm.unregisterNetworkCallback(callback);
+        mCm.unregisterNetworkCallback(fgCallback);
+    }
+
+    @SmallTest
+    public void testRequestBenchmark() throws Exception {
+        // TODO: turn this unit test into a real benchmarking test.
+        // Benchmarks connecting and switching performance in the presence of a large number of
+        // NetworkRequests.
+        // 1. File NUM_REQUESTS requests.
+        // 2. Have a network connect. Wait for NUM_REQUESTS onAvailable callbacks to fire.
+        // 3. Have a new network connect and outscore the previous. Wait for NUM_REQUESTS onLosing
+        //    and NUM_REQUESTS onAvailable callbacks to fire.
+        // See how long it took.
+        final int NUM_REQUESTS = 90;
+        final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
+        final NetworkCallback[] callbacks = new NetworkCallback[NUM_REQUESTS];
+        final CountDownLatch availableLatch = new CountDownLatch(NUM_REQUESTS);
+        final CountDownLatch losingLatch = new CountDownLatch(NUM_REQUESTS);
+
+        for (int i = 0; i < NUM_REQUESTS; i++) {
+            callbacks[i] = new NetworkCallback() {
+                @Override public void onAvailable(Network n) { availableLatch.countDown(); }
+                @Override public void onLosing(Network n, int t) { losingLatch.countDown(); }
+            };
+        }
+
+        final int REGISTER_TIME_LIMIT_MS = 180;
+        assertTimeLimit("Registering callbacks", REGISTER_TIME_LIMIT_MS, () -> {
+            for (NetworkCallback cb : callbacks) {
+                mCm.registerNetworkCallback(request, cb);
+            }
+        });
+
+        final int CONNECT_TIME_LIMIT_MS = 40;
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        // Don't request that the network validate, because otherwise connect() will block until
+        // the network gets NET_CAPABILITY_VALIDATED, after all the callbacks below have fired,
+        // and we won't actually measure anything.
+        mCellNetworkAgent.connect(false);
+
+        long onAvailableDispatchingDuration = durationOf(() -> {
+            if (!awaitLatch(availableLatch, CONNECT_TIME_LIMIT_MS)) {
+                fail(String.format("Only dispatched %d/%d onAvailable callbacks in %dms",
+                        NUM_REQUESTS - availableLatch.getCount(), NUM_REQUESTS,
+                        CONNECT_TIME_LIMIT_MS));
+            }
+        });
+        Log.d(TAG, String.format("Connect, %d callbacks: %dms, acceptable %dms",
+                NUM_REQUESTS, onAvailableDispatchingDuration, CONNECT_TIME_LIMIT_MS));
+
+        final int SWITCH_TIME_LIMIT_MS = 40;
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        // Give wifi a high enough score that we'll linger cell when wifi comes up.
+        mWiFiNetworkAgent.adjustScore(40);
+        mWiFiNetworkAgent.connect(false);
+
+        long onLostDispatchingDuration = durationOf(() -> {
+            if (!awaitLatch(losingLatch, SWITCH_TIME_LIMIT_MS)) {
+                fail(String.format("Only dispatched %d/%d onLosing callbacks in %dms",
+                        NUM_REQUESTS - losingLatch.getCount(), NUM_REQUESTS, SWITCH_TIME_LIMIT_MS));
+            }
+        });
+        Log.d(TAG, String.format("Linger, %d callbacks: %dms, acceptable %dms",
+                NUM_REQUESTS, onLostDispatchingDuration, SWITCH_TIME_LIMIT_MS));
+
+        final int UNREGISTER_TIME_LIMIT_MS = 10;
+        assertTimeLimit("Unregistering callbacks", UNREGISTER_TIME_LIMIT_MS, () -> {
+            for (NetworkCallback cb : callbacks) {
+                mCm.unregisterNetworkCallback(cb);
+            }
+        });
+    }
+
+    private long durationOf(Runnable fn) {
+        long startTime = SystemClock.elapsedRealtime();
+        fn.run();
+        return SystemClock.elapsedRealtime() - startTime;
+    }
+
+    private void assertTimeLimit(String descr, long timeLimit, Runnable fn) {
+        long timeTaken = durationOf(fn);
+        String msg = String.format("%s: took %dms, limit was %dms", descr, timeTaken, timeLimit);
+        Log.d(TAG, msg);
+        assertTrue(msg, timeTaken <= timeLimit);
+    }
+
+    private boolean awaitLatch(CountDownLatch l, long timeoutMs) {
+        try {
+            if (l.await(timeoutMs, TimeUnit.MILLISECONDS)) {
+                return true;
+            }
+        } catch (InterruptedException e) {}
+        return false;
+    }
+
+    @SmallTest
+    public void testMobileDataAlwaysOn() throws Exception {
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.registerNetworkCallback(cellRequest, cellNetworkCallback);
+
+        final HandlerThread handlerThread = new HandlerThread("MobileDataAlwaysOnFactory");
+        handlerThread.start();
+        NetworkCapabilities filter = new NetworkCapabilities()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET);
+        final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "testFactory", filter);
+        testFactory.setScoreFilter(40);
+
+        // Register the factory and expect it to start looking for a network.
+        testFactory.expectAddRequests(1);
+        testFactory.register();
+        testFactory.waitForNetworkRequests(1);
+        assertTrue(testFactory.getMyStartRequested());
+
+        // Bring up wifi. The factory stops looking for a network.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        testFactory.expectAddRequests(2);  // Because the default request changes score twice.
+        mWiFiNetworkAgent.connect(true);
+        testFactory.waitForNetworkRequests(1);
+        assertFalse(testFactory.getMyStartRequested());
+
+        ContentResolver cr = mServiceContext.getContentResolver();
+
+        // Turn on mobile data always on. The factory starts looking again.
+        testFactory.expectAddRequests(1);
+        setMobileDataAlwaysOn(true);
+        testFactory.waitForNetworkRequests(2);
+        assertTrue(testFactory.getMyStartRequested());
+
+        // Bring up cell data and check that the factory stops looking.
+        assertEquals(1, mCm.getAllNetworks().length);
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        testFactory.expectAddRequests(2);  // Because the cell request changes score twice.
+        mCellNetworkAgent.connect(true);
+        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        testFactory.waitForNetworkRequests(2);
+        assertFalse(testFactory.getMyStartRequested());  // Because the cell network outscores us.
+
+        // Check that cell data stays up.
+        mService.waitForIdle();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        assertEquals(2, mCm.getAllNetworks().length);
+
+        // Turn off mobile data always on and expect the request to disappear...
+        testFactory.expectRemoveRequests(1);
+        setMobileDataAlwaysOn(false);
+        testFactory.waitForNetworkRequests(1);
+
+        // ...  and cell data to be torn down.
+        cellNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+        assertEquals(1, mCm.getAllNetworks().length);
+
+        testFactory.unregister();
+        mCm.unregisterNetworkCallback(cellNetworkCallback);
+        handlerThread.quit();
+    }
+
+    @SmallTest
+    public void testAvoidBadWifiSetting() throws Exception {
+        final ContentResolver cr = mServiceContext.getContentResolver();
+        final WrappedAvoidBadWifiTracker tracker = mService.getAvoidBadWifiTracker();
+        final String settingName = Settings.Global.NETWORK_AVOID_BAD_WIFI;
+
+        tracker.configRestrictsAvoidBadWifi = false;
+        String[] values = new String[] {null, "0", "1"};
+        for (int i = 0; i < values.length; i++) {
+            Settings.Global.putInt(cr, settingName, 1);
+            tracker.reevaluate();
+            mService.waitForIdle();
+            String msg = String.format("config=false, setting=%s", values[i]);
+            assertEventuallyTrue(() -> mService.avoidBadWifi(), 50);
+            assertFalse(msg, tracker.shouldNotifyWifiUnvalidated());
+        }
+
+        tracker.configRestrictsAvoidBadWifi = true;
+
+        Settings.Global.putInt(cr, settingName, 0);
+        tracker.reevaluate();
+        mService.waitForIdle();
+        assertEventuallyTrue(() -> !mService.avoidBadWifi(), 50);
+        assertFalse(tracker.shouldNotifyWifiUnvalidated());
+
+        Settings.Global.putInt(cr, settingName, 1);
+        tracker.reevaluate();
+        mService.waitForIdle();
+        assertEventuallyTrue(() -> mService.avoidBadWifi(), 50);
+        assertFalse(tracker.shouldNotifyWifiUnvalidated());
+
+        Settings.Global.putString(cr, settingName, null);
+        tracker.reevaluate();
+        mService.waitForIdle();
+        assertEventuallyTrue(() -> !mService.avoidBadWifi(), 50);
+        assertTrue(tracker.shouldNotifyWifiUnvalidated());
+    }
+
+    @SmallTest
+    public void testAvoidBadWifi() throws Exception {
+        final ContentResolver cr = mServiceContext.getContentResolver();
+        final WrappedAvoidBadWifiTracker tracker = mService.getAvoidBadWifiTracker();
+
+        // Pretend we're on a carrier that restricts switching away from bad wifi.
+        tracker.configRestrictsAvoidBadWifi = true;
+
+        // File a request for cell to ensure it doesn't go down.
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.requestNetwork(cellRequest, cellNetworkCallback);
+
+        TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+
+        NetworkRequest validatedWifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_VALIDATED)
+                .build();
+        TestNetworkCallback validatedWifiCallback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(validatedWifiRequest, validatedWifiCallback);
+
+        Settings.Global.putInt(cr, Settings.Global.NETWORK_AVOID_BAD_WIFI, 0);
+        tracker.reevaluate();
+
+        // Bring up validated cell.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        Network cellNetwork = mCellNetworkAgent.getNetwork();
+
+        // Bring up validated wifi.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        validatedWifiCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        Network wifiNetwork = mWiFiNetworkAgent.getNetwork();
+
+        // Fail validation on wifi.
+        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 599;
+        mCm.reportNetworkConnectivity(wifiNetwork, false);
+        validatedWifiCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+
+        // Because avoid bad wifi is off, we don't switch to cellular.
+        defaultCallback.assertNoCallback();
+        assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertEquals(mCm.getActiveNetwork(), wifiNetwork);
+
+        // Simulate switching to a carrier that does not restrict avoiding bad wifi, and expect
+        // that we switch back to cell.
+        tracker.configRestrictsAvoidBadWifi = false;
+        tracker.reevaluate();
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
+        // Switch back to a restrictive carrier.
+        tracker.configRestrictsAvoidBadWifi = true;
+        tracker.reevaluate();
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        assertEquals(mCm.getActiveNetwork(), wifiNetwork);
+
+        // Simulate the user selecting "switch" on the dialog, and check that we switch to cell.
+        mCm.setAvoidUnvalidated(wifiNetwork);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
+        // Disconnect and reconnect wifi to clear the one-time switch above.
+        mWiFiNetworkAgent.disconnect();
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        validatedWifiCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        wifiNetwork = mWiFiNetworkAgent.getNetwork();
+
+        // Fail validation on wifi and expect the dialog to appear.
+        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 599;
+        mCm.reportNetworkConnectivity(wifiNetwork, false);
+        validatedWifiCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+
+        // Simulate the user selecting "switch" and checking the don't ask again checkbox.
+        Settings.Global.putInt(cr, Settings.Global.NETWORK_AVOID_BAD_WIFI, 1);
+        tracker.reevaluate();
+
+        // We now switch to cell.
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
+        // Simulate the user turning the cellular fallback setting off and then on.
+        // We switch to wifi and then to cell.
+        Settings.Global.putString(cr, Settings.Global.NETWORK_AVOID_BAD_WIFI, null);
+        tracker.reevaluate();
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        assertEquals(mCm.getActiveNetwork(), wifiNetwork);
+        Settings.Global.putInt(cr, Settings.Global.NETWORK_AVOID_BAD_WIFI, 1);
+        tracker.reevaluate();
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
+        // If cell goes down, we switch to wifi.
+        mCellNetworkAgent.disconnect();
+        defaultCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        validatedWifiCallback.assertNoCallback();
+
+        mCm.unregisterNetworkCallback(cellNetworkCallback);
+        mCm.unregisterNetworkCallback(validatedWifiCallback);
+        mCm.unregisterNetworkCallback(defaultCallback);
+    }
+
+    /**
+     * Validate that a satisfied network request does not trigger onUnavailable() once the
+     * time-out period expires.
+     */
+    @SmallTest
+    public void testSatisfiedNetworkRequestDoesNotTriggerOnUnavailable() {
+        NetworkRequest nr = new NetworkRequest.Builder().addTransportType(
+                NetworkCapabilities.TRANSPORT_WIFI).build();
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.requestNetwork(nr, networkCallback, 10);
+
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        networkCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+
+        // pass timeout and validate that UNAVAILABLE is not called
+        sleepFor(15);
+        networkCallback.assertNoCallback();
+    }
+
+    /**
+     * Validate that a satisfied network request followed by a disconnected (lost) network does
+     * not trigger onUnavailable() once the time-out period expires.
+     */
+    @SmallTest
+    public void testSatisfiedThenLostNetworkRequestDoesNotTriggerOnUnavailable() {
+        NetworkRequest nr = new NetworkRequest.Builder().addTransportType(
+                NetworkCapabilities.TRANSPORT_WIFI).build();
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.requestNetwork(nr, networkCallback, 500);
+
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        networkCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        sleepFor(20);
+        mWiFiNetworkAgent.disconnect();
+        networkCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+
+        // pass timeout and validate that UNAVAILABLE is not called
+        sleepFor(600);
+        networkCallback.assertNoCallback();
+    }
+
+    /**
+     * Validate that when a time-out is specified for a network request the onUnavailable()
+     * callback is called when time-out expires. Then validate that if network request is
+     * (somehow) satisfied - the callback isn't called later.
+     */
+    @SmallTest
+    public void testTimedoutNetworkRequest() {
+        NetworkRequest nr = new NetworkRequest.Builder().addTransportType(
+                NetworkCapabilities.TRANSPORT_WIFI).build();
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.requestNetwork(nr, networkCallback, 10);
+
+        // pass timeout and validate that UNAVAILABLE is called
+        networkCallback.expectCallback(CallbackState.UNAVAILABLE, null);
+
+        // create a network satisfying request - validate that request not triggered
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        networkCallback.assertNoCallback();
+    }
+
+    /**
+     * Validate that when a network request is unregistered (cancelled) the time-out for that
+     * request doesn't trigger the onUnavailable() callback.
+     */
+    @SmallTest
+    public void testTimedoutAfterUnregisteredNetworkRequest() {
+        NetworkRequest nr = new NetworkRequest.Builder().addTransportType(
+                NetworkCapabilities.TRANSPORT_WIFI).build();
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.requestNetwork(nr, networkCallback, 10);
+
+        // remove request
+        mCm.unregisterNetworkCallback(networkCallback);
+
+        // pass timeout and validate that no callbacks
+        // Note: doesn't validate that nothing called from CS since even if called the CM already
+        // unregisters the callback and won't pass it through!
+        sleepFor(15);
+        networkCallback.assertNoCallback();
+
+        // create a network satisfying request - validate that request not triggered
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        networkCallback.assertNoCallback();
+    }
+
+    public void assertEventuallyTrue(BooleanSupplier fn, long maxWaitingTimeMs) throws Exception {
+        long start = SystemClock.elapsedRealtime();
+        while (SystemClock.elapsedRealtime() <= start + maxWaitingTimeMs) {
+            if (fn.getAsBoolean()) {
+                return;
+            }
+            Thread.sleep(10);
+        }
+        assertTrue(fn.getAsBoolean());
+    }
+
+    private static class TestKeepaliveCallback extends PacketKeepaliveCallback {
+
+        public static enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR };
+
+        private class CallbackValue {
+            public CallbackType callbackType;
+            public int error;
+
+            public CallbackValue(CallbackType type) {
+                this.callbackType = type;
+                this.error = PacketKeepalive.SUCCESS;
+                assertTrue("onError callback must have error", type != CallbackType.ON_ERROR);
+            }
+
+            public CallbackValue(CallbackType type, int error) {
+                this.callbackType = type;
+                this.error = error;
+                assertEquals("error can only be set for onError", type, CallbackType.ON_ERROR);
+            }
+
+            @Override
+            public boolean equals(Object o) {
+                return o instanceof CallbackValue &&
+                        this.callbackType == ((CallbackValue) o).callbackType &&
+                        this.error == ((CallbackValue) o).error;
+            }
+
+            @Override
+            public String toString() {
+                return String.format("%s(%s, %d)", getClass().getSimpleName(), callbackType, error);
+            }
+        }
+
+        private LinkedBlockingQueue<CallbackValue> mCallbacks = new LinkedBlockingQueue<>();
+
+        @Override
+        public void onStarted() {
+            mCallbacks.add(new CallbackValue(CallbackType.ON_STARTED));
+        }
+
+        @Override
+        public void onStopped() {
+            mCallbacks.add(new CallbackValue(CallbackType.ON_STOPPED));
+        }
+
+        @Override
+        public void onError(int error) {
+            mCallbacks.add(new CallbackValue(CallbackType.ON_ERROR, error));
+        }
+
+        private void expectCallback(CallbackValue callbackValue) {
+            try {
+                assertEquals(
+                        callbackValue,
+                        mCallbacks.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            } catch (InterruptedException e) {
+                fail(callbackValue.callbackType + " callback not seen after " + TIMEOUT_MS + " ms");
+            }
+        }
+
+        public void expectStarted() {
+            expectCallback(new CallbackValue(CallbackType.ON_STARTED));
+        }
+
+        public void expectStopped() {
+            expectCallback(new CallbackValue(CallbackType.ON_STOPPED));
+        }
+
+        public void expectError(int error) {
+            expectCallback(new CallbackValue(CallbackType.ON_ERROR, error));
+        }
+    }
+
+    private Network connectKeepaliveNetwork(LinkProperties lp) {
+        // Ensure the network is disconnected before we do anything.
+        if (mWiFiNetworkAgent != null) {
+            assertNull(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()));
+        }
+
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mWiFiNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.sendLinkProperties(lp);
+        mService.waitForIdle();
+        return mWiFiNetworkAgent.getNetwork();
+    }
+
+    @SmallTest
+    public void testPacketKeepalives() throws Exception {
+        InetAddress myIPv4 = InetAddress.getByName("192.0.2.129");
+        InetAddress notMyIPv4 = InetAddress.getByName("192.0.2.35");
+        InetAddress myIPv6 = InetAddress.getByName("2001:db8::1");
+        InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8");
+        InetAddress dstIPv6 = InetAddress.getByName("2001:4860:4860::8888");
+
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("wlan12");
+        lp.addLinkAddress(new LinkAddress(myIPv6, 64));
+        lp.addLinkAddress(new LinkAddress(myIPv4, 25));
+        lp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234")));
+        lp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254")));
+
+        Network notMyNet = new Network(61234);
+        Network myNet = connectKeepaliveNetwork(lp);
+
+        TestKeepaliveCallback callback = new TestKeepaliveCallback();
+        PacketKeepalive ka;
+
+        // Attempt to start keepalives with invalid parameters and check for errors.
+        ka = mCm.startNattKeepalive(notMyNet, 25, callback, myIPv4, 1234, dstIPv4);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_NETWORK);
+
+        ka = mCm.startNattKeepalive(myNet, 19, callback, notMyIPv4, 1234, dstIPv4);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_INTERVAL);
+
+        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 1234, dstIPv6);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
+
+        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv6, 1234, dstIPv4);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
+
+        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv6, 1234, dstIPv6);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS);  // NAT-T is IPv4-only.
+
+        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 123456, dstIPv4);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_PORT);
+
+        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 123456, dstIPv4);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_PORT);
+
+        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 12345, dstIPv4);
+        callback.expectError(PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED);
+
+        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 12345, dstIPv4);
+        callback.expectError(PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED);
+
+        // Check that a started keepalive can be stopped.
+        mWiFiNetworkAgent.setStartKeepaliveError(PacketKeepalive.SUCCESS);
+        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 12345, dstIPv4);
+        callback.expectStarted();
+        mWiFiNetworkAgent.setStopKeepaliveError(PacketKeepalive.SUCCESS);
+        ka.stop();
+        callback.expectStopped();
+
+        // Check that deleting the IP address stops the keepalive.
+        LinkProperties bogusLp = new LinkProperties(lp);
+        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 12345, dstIPv4);
+        callback.expectStarted();
+        bogusLp.removeLinkAddress(new LinkAddress(myIPv4, 25));
+        bogusLp.addLinkAddress(new LinkAddress(notMyIPv4, 25));
+        mWiFiNetworkAgent.sendLinkProperties(bogusLp);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
+        mWiFiNetworkAgent.sendLinkProperties(lp);
+
+        // Check that a started keepalive is stopped correctly when the network disconnects.
+        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 12345, dstIPv4);
+        callback.expectStarted();
+        mWiFiNetworkAgent.disconnect();
+        callback.expectError(PacketKeepalive.ERROR_INVALID_NETWORK);
+
+        // ... and that stopping it after that has no adverse effects.
+        assertNull(mCm.getNetworkCapabilities(myNet));
+        ka.stop();
+
+        // Reconnect.
+        myNet = connectKeepaliveNetwork(lp);
+        mWiFiNetworkAgent.setStartKeepaliveError(PacketKeepalive.SUCCESS);
+
+        // Check things work as expected when the keepalive is stopped and the network disconnects.
+        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 12345, dstIPv4);
+        callback.expectStarted();
+        ka.stop();
+        mWiFiNetworkAgent.disconnect();
+        mService.waitForIdle();
+        callback.expectStopped();
+
+        // Reconnect.
+        myNet = connectKeepaliveNetwork(lp);
+        mWiFiNetworkAgent.setStartKeepaliveError(PacketKeepalive.SUCCESS);
+
+        // Check that keepalive slots start from 1 and increment. The first one gets slot 1.
+        mWiFiNetworkAgent.setExpectedKeepaliveSlot(1);
+        ka = mCm.startNattKeepalive(myNet, 25, callback, myIPv4, 12345, dstIPv4);
+        callback.expectStarted();
+
+        // The second one gets slot 2.
+        mWiFiNetworkAgent.setExpectedKeepaliveSlot(2);
+        TestKeepaliveCallback callback2 = new TestKeepaliveCallback();
+        PacketKeepalive ka2 = mCm.startNattKeepalive(myNet, 25, callback2, myIPv4, 6789, dstIPv4);
+        callback2.expectStarted();
+
+        // Now stop the first one and create a third. This also gets slot 1.
+        ka.stop();
+        callback.expectStopped();
+
+        mWiFiNetworkAgent.setExpectedKeepaliveSlot(1);
+        TestKeepaliveCallback callback3 = new TestKeepaliveCallback();
+        PacketKeepalive ka3 = mCm.startNattKeepalive(myNet, 25, callback3, myIPv4, 9876, dstIPv4);
+        callback3.expectStarted();
+
+        ka2.stop();
+        callback2.expectStopped();
+
+        ka3.stop();
+        callback3.expectStopped();
+    }
+
+    @SmallTest
+    public void testGetCaptivePortalServerUrl() throws Exception {
+        String url = mCm.getCaptivePortalServerUrl();
+        assertEquals("http://connectivitycheck.gstatic.com/generate_204", url);
+    }
+
+    private static class TestNetworkPinner extends NetworkPinner {
+        public static boolean awaitPin(int timeoutMs) {
+            synchronized(sLock) {
+                if (sNetwork == null) {
+                    try {
+                        sLock.wait(timeoutMs);
+                    } catch (InterruptedException e) {}
+                }
+                return sNetwork != null;
+            }
+        }
+
+        public static boolean awaitUnpin(int timeoutMs) {
+            synchronized(sLock) {
+                if (sNetwork != null) {
+                    try {
+                        sLock.wait(timeoutMs);
+                    } catch (InterruptedException e) {}
+                }
+                return sNetwork == null;
+            }
+        }
+    }
+
+    private void assertPinnedToWifiWithCellDefault() {
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getBoundNetworkForProcess());
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+    }
+
+    private void assertPinnedToWifiWithWifiDefault() {
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getBoundNetworkForProcess());
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+    }
+
+    private void assertNotPinnedToWifi() {
+        assertNull(mCm.getBoundNetworkForProcess());
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+    }
+
+    @SmallTest
+    public void testNetworkPinner() {
+        NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .build();
+        assertNull(mCm.getBoundNetworkForProcess());
+
+        TestNetworkPinner.pin(mServiceContext, wifiRequest);
+        assertNull(mCm.getBoundNetworkForProcess());
+
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+
+        // When wi-fi connects, expect to be pinned.
+        assertTrue(TestNetworkPinner.awaitPin(100));
+        assertPinnedToWifiWithCellDefault();
+
+        // Disconnect and expect the pin to drop.
+        mWiFiNetworkAgent.disconnect();
+        assertTrue(TestNetworkPinner.awaitUnpin(100));
+        assertNotPinnedToWifi();
+
+        // Reconnecting does not cause the pin to come back.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        assertFalse(TestNetworkPinner.awaitPin(100));
+        assertNotPinnedToWifi();
+
+        // Pinning while connected causes the pin to take effect immediately.
+        TestNetworkPinner.pin(mServiceContext, wifiRequest);
+        assertTrue(TestNetworkPinner.awaitPin(100));
+        assertPinnedToWifiWithCellDefault();
+
+        // Explicitly unpin and expect to use the default network again.
+        TestNetworkPinner.unpin();
+        assertNotPinnedToWifi();
+
+        // Disconnect cell and wifi.
+        ConditionVariable cv = waitForConnectivityBroadcasts(3);  // cell down, wifi up, wifi down.
+        mCellNetworkAgent.disconnect();
+        mWiFiNetworkAgent.disconnect();
+        waitFor(cv);
+
+        // Pinning takes effect even if the pinned network is the default when the pin is set...
+        TestNetworkPinner.pin(mServiceContext, wifiRequest);
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        assertTrue(TestNetworkPinner.awaitPin(100));
+        assertPinnedToWifiWithWifiDefault();
+
+        // ... and is maintained even when that network is no longer the default.
+        cv = waitForConnectivityBroadcasts(1);
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mCellNetworkAgent.connect(true);
+        waitFor(cv);
+        assertPinnedToWifiWithCellDefault();
+    }
+
+    @SmallTest
+    public void testNetworkRequestMaximum() {
+        final int MAX_REQUESTS = 100;
+        // Test that the limit is enforced when MAX_REQUESTS simultaneous requests are added.
+        NetworkRequest networkRequest = new NetworkRequest.Builder().build();
+        ArrayList<NetworkCallback> networkCallbacks = new ArrayList<NetworkCallback>();
+        try {
+            for (int i = 0; i < MAX_REQUESTS; i++) {
+                NetworkCallback networkCallback = new NetworkCallback();
+                mCm.requestNetwork(networkRequest, networkCallback);
+                networkCallbacks.add(networkCallback);
+            }
+            fail("Registering " + MAX_REQUESTS + " NetworkRequests did not throw exception");
+        } catch (IllegalArgumentException expected) {}
+        for (NetworkCallback networkCallback : networkCallbacks) {
+            mCm.unregisterNetworkCallback(networkCallback);
+        }
+        networkCallbacks.clear();
+
+        try {
+            for (int i = 0; i < MAX_REQUESTS; i++) {
+                NetworkCallback networkCallback = new NetworkCallback();
+                mCm.registerNetworkCallback(networkRequest, networkCallback);
+                networkCallbacks.add(networkCallback);
+            }
+            fail("Registering " + MAX_REQUESTS + " NetworkCallbacks did not throw exception");
+        } catch (IllegalArgumentException expected) {}
+        for (NetworkCallback networkCallback : networkCallbacks) {
+            mCm.unregisterNetworkCallback(networkCallback);
+        }
+        networkCallbacks.clear();
+
+        ArrayList<PendingIntent> pendingIntents = new ArrayList<PendingIntent>();
+        try {
+            for (int i = 0; i < MAX_REQUESTS + 1; i++) {
+                PendingIntent pendingIntent =
+                        PendingIntent.getBroadcast(mContext, 0, new Intent("a" + i), 0);
+                mCm.requestNetwork(networkRequest, pendingIntent);
+                pendingIntents.add(pendingIntent);
+            }
+            fail("Registering " + MAX_REQUESTS +
+                    " PendingIntent NetworkRequests did not throw exception");
+        } catch (IllegalArgumentException expected) {}
+        for (PendingIntent pendingIntent : pendingIntents) {
+            mCm.unregisterNetworkCallback(pendingIntent);
+        }
+        pendingIntents.clear();
+
+        try {
+            for (int i = 0; i < MAX_REQUESTS + 1; i++) {
+                PendingIntent pendingIntent =
+                        PendingIntent.getBroadcast(mContext, 0, new Intent("a" + i), 0);
+                mCm.registerNetworkCallback(networkRequest, pendingIntent);
+                pendingIntents.add(pendingIntent);
+            }
+            fail("Registering " + MAX_REQUESTS +
+                    " PendingIntent NetworkCallbacks did not throw exception");
+        } catch (IllegalArgumentException expected) {}
+        for (PendingIntent pendingIntent : pendingIntents) {
+            mCm.unregisterNetworkCallback(pendingIntent);
+        }
+        pendingIntents.clear();
+        mService.waitForIdle(5000);
+
+        // Test that the limit is not hit when MAX_REQUESTS requests are added and removed.
+        for (int i = 0; i < MAX_REQUESTS; i++) {
+            NetworkCallback networkCallback = new NetworkCallback();
+            mCm.requestNetwork(networkRequest, networkCallback);
+            mCm.unregisterNetworkCallback(networkCallback);
+        }
+        mService.waitForIdle();
+        for (int i = 0; i < MAX_REQUESTS; i++) {
+            NetworkCallback networkCallback = new NetworkCallback();
+            mCm.registerNetworkCallback(networkRequest, networkCallback);
+            mCm.unregisterNetworkCallback(networkCallback);
+        }
+        mService.waitForIdle();
+        for (int i = 0; i < MAX_REQUESTS; i++) {
+            PendingIntent pendingIntent =
+                    PendingIntent.getBroadcast(mContext, 0, new Intent("b" + i), 0);
+            mCm.requestNetwork(networkRequest, pendingIntent);
+            mCm.unregisterNetworkCallback(pendingIntent);
+        }
+        mService.waitForIdle();
+        for (int i = 0; i < MAX_REQUESTS; i++) {
+            PendingIntent pendingIntent =
+                    PendingIntent.getBroadcast(mContext, 0, new Intent("c" + i), 0);
+            mCm.registerNetworkCallback(networkRequest, pendingIntent);
+            mCm.unregisterNetworkCallback(pendingIntent);
+        }
+    }
+
+    /* test utilities */
+    static private void sleepFor(int ms) {
+        try {
+            Thread.sleep(ms);
+        } catch (InterruptedException e) {
+        }
+
+    }
+}
diff --git a/tests/net/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java b/tests/net/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
new file mode 100644
index 0000000..84f0f90
--- /dev/null
+++ b/tests/net/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2016 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.connectivity;
+
+import android.net.ConnectivityMetricsEvent;
+import android.net.metrics.ApfProgramEvent;
+import android.net.metrics.ApfStats;
+import android.net.metrics.DefaultNetworkEvent;
+import android.net.metrics.DhcpClientEvent;
+import android.net.metrics.DhcpErrorEvent;
+import android.net.metrics.DnsEvent;
+import android.net.metrics.IpManagerEvent;
+import android.net.metrics.IpReachabilityEvent;
+import android.net.metrics.NetworkEvent;
+import android.net.metrics.RaEvent;
+import android.net.metrics.ValidationProbeEvent;
+import com.google.protobuf.nano.MessageNano;
+import java.util.Arrays;
+import junit.framework.TestCase;
+
+import static com.android.server.connectivity.metrics.IpConnectivityLogClass.IpConnectivityLog;
+import static com.android.server.connectivity.MetricsTestUtil.aBool;
+import static com.android.server.connectivity.MetricsTestUtil.aByteArray;
+import static com.android.server.connectivity.MetricsTestUtil.aLong;
+import static com.android.server.connectivity.MetricsTestUtil.aString;
+import static com.android.server.connectivity.MetricsTestUtil.aType;
+import static com.android.server.connectivity.MetricsTestUtil.anInt;
+import static com.android.server.connectivity.MetricsTestUtil.anIntArray;
+import static com.android.server.connectivity.MetricsTestUtil.b;
+import static com.android.server.connectivity.MetricsTestUtil.describeIpEvent;
+import static com.android.server.connectivity.MetricsTestUtil.ipEv;
+
+public class IpConnectivityEventBuilderTest extends TestCase {
+
+    public void testDefaultNetworkEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(DefaultNetworkEvent.class),
+                anInt(102),
+                anIntArray(1, 2, 3),
+                anInt(101),
+                aBool(true),
+                aBool(false));
+
+        String want = joinLines(
+                "dropped_events: 0",
+                "events <",
+                "  default_network_event <",
+                "    network_id <",
+                "      network_id: 102",
+                "    >",
+                "    previous_network_id <",
+                "      network_id: 101",
+                "    >",
+                "    previous_network_ip_support: 1",
+                "    transport_types: 1",
+                "    transport_types: 2",
+                "    transport_types: 3",
+                "  >",
+                "  time_ms: 1",
+                ">",
+                "version: 2");
+
+        verifySerialization(want, ev);
+    }
+
+    public void testDhcpClientEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(DhcpClientEvent.class),
+                aString("wlan0"),
+                aString("SomeState"),
+                anInt(192));
+
+        String want = joinLines(
+                "dropped_events: 0",
+                "events <",
+                "  dhcp_event <",
+                "    duration_ms: 192",
+                "    error_code: 0",
+                "    if_name: \"wlan0\"",
+                "    state_transition: \"SomeState\"",
+                "  >",
+                "  time_ms: 1",
+                ">",
+                "version: 2");
+
+        verifySerialization(want, ev);
+    }
+
+    public void testDhcpErrorEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(DhcpErrorEvent.class),
+                aString("wlan0"),
+                anInt(DhcpErrorEvent.L4_NOT_UDP));
+
+        String want = joinLines(
+                "dropped_events: 0",
+                "events <",
+                "  dhcp_event <",
+                "    duration_ms: 0",
+                "    error_code: 50397184",
+                "    if_name: \"wlan0\"",
+                "    state_transition: \"\"",
+                "  >",
+                "  time_ms: 1",
+                ">",
+                "version: 2");
+
+        verifySerialization(want, ev);
+    }
+
+    public void testDnsEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(DnsEvent.class),
+                anInt(101),
+                aByteArray(b(1), b(1), b(2), b(1), b(1), b(1), b(2), b(2)),
+                aByteArray(b(0), b(0), b(22), b(3), b(1), b(0), b(200), b(178)),
+                anIntArray(3456, 267, 1230, 45, 2111, 450, 638, 1300));
+
+        String want = joinLines(
+                "dropped_events: 0",
+                "events <",
+                "  dns_lookup_batch <",
+                "    event_types: 1",
+                "    event_types: 1",
+                "    event_types: 2",
+                "    event_types: 1",
+                "    event_types: 1",
+                "    event_types: 1",
+                "    event_types: 2",
+                "    event_types: 2",
+                "    latencies_ms: 3456",
+                "    latencies_ms: 267",
+                "    latencies_ms: 1230",
+                "    latencies_ms: 45",
+                "    latencies_ms: 2111",
+                "    latencies_ms: 450",
+                "    latencies_ms: 638",
+                "    latencies_ms: 1300",
+                "    network_id <",
+                "      network_id: 101",
+                "    >",
+                "    return_codes: 0",
+                "    return_codes: 0",
+                "    return_codes: 22",
+                "    return_codes: 3",
+                "    return_codes: 1",
+                "    return_codes: 0",
+                "    return_codes: 200",
+                "    return_codes: 178",
+                "  >",
+                "  time_ms: 1",
+                ">",
+                "version: 2");
+
+        verifySerialization(want, ev);
+    }
+
+    public void testIpManagerEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(IpManagerEvent.class),
+                aString("wlan0"),
+                anInt(IpManagerEvent.PROVISIONING_OK),
+                aLong(5678));
+
+        String want = joinLines(
+                "dropped_events: 0",
+                "events <",
+                "  ip_provisioning_event <",
+                "    event_type: 1",
+                "    if_name: \"wlan0\"",
+                "    latency_ms: 5678",
+                "  >",
+                "  time_ms: 1",
+                ">",
+                "version: 2");
+
+        verifySerialization(want, ev);
+    }
+
+    public void testIpReachabilityEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(IpReachabilityEvent.class),
+                aString("wlan0"),
+                anInt(IpReachabilityEvent.NUD_FAILED));
+
+        String want = joinLines(
+                "dropped_events: 0",
+                "events <",
+                "  ip_reachability_event <",
+                "    event_type: 512",
+                "    if_name: \"wlan0\"",
+                "  >",
+                "  time_ms: 1",
+                ">",
+                "version: 2");
+
+        verifySerialization(want, ev);
+    }
+
+    public void testNetworkEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(NetworkEvent.class),
+                anInt(100),
+                anInt(5),
+                aLong(20410));
+
+        String want = joinLines(
+                "dropped_events: 0",
+                "events <",
+                "  network_event <",
+                "    event_type: 5",
+                "    latency_ms: 20410",
+                "    network_id <",
+                "      network_id: 100",
+                "    >",
+                "  >",
+                "  time_ms: 1",
+                ">",
+                "version: 2");
+
+        verifySerialization(want, ev);
+    }
+
+    public void testValidationProbeEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(ValidationProbeEvent.class),
+                anInt(120),
+                aLong(40730),
+                anInt(ValidationProbeEvent.PROBE_HTTP),
+                anInt(204));
+
+        String want = joinLines(
+                "dropped_events: 0",
+                "events <",
+                "  time_ms: 1",
+                "  validation_probe_event <",
+                "    latency_ms: 40730",
+                "    network_id <",
+                "      network_id: 120",
+                "    >",
+                "    probe_result: 204",
+                "    probe_type: 1",
+                "  >",
+                ">",
+                "version: 2");
+
+        verifySerialization(want, ev);
+    }
+
+    public void testApfProgramEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(ApfProgramEvent.class),
+                aLong(200),
+                anInt(7),
+                anInt(9),
+                anInt(2048),
+                anInt(3));
+
+        String want = joinLines(
+                "dropped_events: 0",
+                "events <",
+                "  apf_program_event <",
+                "    current_ras: 9",
+                "    drop_multicast: true",
+                "    filtered_ras: 7",
+                "    has_ipv4_addr: true",
+                "    lifetime: 200",
+                "    program_length: 2048",
+                "  >",
+                "  time_ms: 1",
+                ">",
+                "version: 2");
+
+        verifySerialization(want, ev);
+    }
+
+    public void testApfStatsSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(ApfStats.class),
+                aLong(45000),
+                anInt(10),
+                anInt(2),
+                anInt(2),
+                anInt(1),
+                anInt(2),
+                anInt(4),
+                anInt(2048));
+
+        String want = joinLines(
+                "dropped_events: 0",
+                "events <",
+                "  apf_statistics <",
+                "    dropped_ras: 2",
+                "    duration_ms: 45000",
+                "    matching_ras: 2",
+                "    max_program_size: 2048",
+                "    parse_errors: 2",
+                "    program_updates: 4",
+                "    received_ras: 10",
+                "    zero_lifetime_ras: 1",
+                "  >",
+                "  time_ms: 1",
+                ">",
+                "version: 2");
+
+        verifySerialization(want, ev);
+    }
+
+    public void testRaEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(RaEvent.class),
+                aLong(2000),
+                aLong(400),
+                aLong(300),
+                aLong(-1),
+                aLong(1000),
+                aLong(-1));
+
+        String want = joinLines(
+                "dropped_events: 0",
+                "events <",
+                "  ra_event <",
+                "    dnssl_lifetime: -1",
+                "    prefix_preferred_lifetime: 300",
+                "    prefix_valid_lifetime: 400",
+                "    rdnss_lifetime: 1000",
+                "    route_info_lifetime: -1",
+                "    router_lifetime: 2000",
+                "  >",
+                "  time_ms: 1",
+                ">",
+                "version: 2");
+
+        verifySerialization(want, ev);
+    }
+
+    static void verifySerialization(String want, ConnectivityMetricsEvent... input) {
+        try {
+            byte[] got = IpConnectivityEventBuilder.serialize(0, Arrays.asList(input));
+            IpConnectivityLog log = new IpConnectivityLog();
+            MessageNano.mergeFrom(log, got);
+            assertEquals(want, log.toString());
+        } catch (Exception e) {
+            fail(e.toString());
+        }
+    }
+
+    static String joinLines(String ... elems) {
+        StringBuilder b = new StringBuilder();
+        for (String s : elems) {
+            b.append(s);
+            b.append("\n");
+        }
+        return b.toString();
+    }
+}
diff --git a/tests/net/java/com/android/server/connectivity/IpConnectivityMetricsTest.java b/tests/net/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
new file mode 100644
index 0000000..aa491bb
--- /dev/null
+++ b/tests/net/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2016, 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.connectivity;
+
+import android.content.Context;
+import android.net.ConnectivityMetricsEvent;
+import android.net.IIpConnectivityMetrics;
+import android.net.metrics.ApfProgramEvent;
+import android.net.metrics.ApfStats;
+import android.net.metrics.DefaultNetworkEvent;
+import android.net.metrics.DhcpClientEvent;
+import android.net.metrics.IpConnectivityLog;
+import android.net.metrics.IpManagerEvent;
+import android.net.metrics.IpReachabilityEvent;
+import android.net.metrics.RaEvent;
+import android.net.metrics.ValidationProbeEvent;
+import android.os.Parcelable;
+import android.util.Base64;
+import com.android.server.connectivity.metrics.IpConnectivityLogClass;
+import com.google.protobuf.nano.MessageNano;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import junit.framework.TestCase;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class IpConnectivityMetricsTest extends TestCase {
+    static final IpReachabilityEvent FAKE_EV =
+            new IpReachabilityEvent("wlan0", IpReachabilityEvent.NUD_FAILED);
+
+    @Mock Context mCtx;
+    @Mock IIpConnectivityMetrics mMockService;
+
+    IpConnectivityMetrics mService;
+
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mService = new IpConnectivityMetrics(mCtx, (ctx) -> 2000);
+    }
+
+    public void testLoggingEvents() throws Exception {
+        IpConnectivityLog logger = new IpConnectivityLog(mMockService);
+
+        assertTrue(logger.log(1, FAKE_EV));
+        assertTrue(logger.log(2, FAKE_EV));
+        assertTrue(logger.log(3, FAKE_EV));
+
+        List<ConnectivityMetricsEvent> got = verifyEvents(3);
+        assertEventsEqual(expectedEvent(1), got.get(0));
+        assertEventsEqual(expectedEvent(2), got.get(1));
+        assertEventsEqual(expectedEvent(3), got.get(2));
+    }
+
+    public void testLoggingEventsWithMultipleCallers() throws Exception {
+        IpConnectivityLog logger = new IpConnectivityLog(mMockService);
+
+        final int nCallers = 10;
+        final int nEvents = 10;
+        for (int n = 0; n < nCallers; n++) {
+            final int i = n;
+            new Thread() {
+                public void run() {
+                    for (int j = 0; j < nEvents; j++) {
+                        assertTrue(logger.log(i * 100 + j, FAKE_EV));
+                    }
+                }
+            }.start();
+        }
+
+        List<ConnectivityMetricsEvent> got = verifyEvents(nCallers * nEvents, 100);
+        Collections.sort(got, EVENT_COMPARATOR);
+        Iterator<ConnectivityMetricsEvent> iter = got.iterator();
+        for (int i = 0; i < nCallers; i++) {
+            for (int j = 0; j < nEvents; j++) {
+                int expectedTimestamp = i * 100 + j;
+                assertEventsEqual(expectedEvent(expectedTimestamp), iter.next());
+            }
+        }
+    }
+
+    public void testBufferFlushing() {
+        String output1 = getdump("flush");
+        assertEquals("", output1);
+
+        new IpConnectivityLog(mService.impl).log(1, FAKE_EV);
+        String output2 = getdump("flush");
+        assertFalse("".equals(output2));
+
+        String output3 = getdump("flush");
+        assertEquals("", output3);
+    }
+
+    public void testRateLimiting() {
+        final IpConnectivityLog logger = new IpConnectivityLog(mService.impl);
+        final ApfProgramEvent ev = new ApfProgramEvent(0, 0, 0, 0, 0);
+        final long fakeTimestamp = 1;
+
+        int attempt = 100; // More than burst quota, but less than buffer size.
+        for (int i = 0; i < attempt; i++) {
+            logger.log(ev);
+        }
+
+        String output1 = getdump("flush");
+        assertFalse("".equals(output1));
+
+        for (int i = 0; i < attempt; i++) {
+            assertFalse("expected event to be dropped", logger.log(fakeTimestamp, ev));
+        }
+
+        String output2 = getdump("flush");
+        assertEquals("", output2);
+    }
+
+    public void testEndToEndLogging() {
+        IpConnectivityLog logger = new IpConnectivityLog(mService.impl);
+
+        Parcelable[] events = {
+            new IpReachabilityEvent("wlan0", IpReachabilityEvent.NUD_FAILED),
+            new DhcpClientEvent("wlan0", "SomeState", 192),
+            new DefaultNetworkEvent(102, new int[]{1,2,3}, 101, true, false),
+            new IpManagerEvent("wlan0", IpManagerEvent.PROVISIONING_OK, 5678),
+            new ValidationProbeEvent(120, 40730, ValidationProbeEvent.PROBE_HTTP, 204),
+            new ApfStats(45000, 10, 2, 2, 1, 2, 4, 2048),
+            new RaEvent(2000, 400, 300, -1, 1000, -1)
+        };
+
+        for (int i = 0; i < events.length; i++) {
+            logger.log(100 * (i + 1), events[i]);
+        }
+
+        String want = joinLines(
+                "dropped_events: 0",
+                "events <",
+                "  ip_reachability_event <",
+                "    event_type: 512",
+                "    if_name: \"wlan0\"",
+                "  >",
+                "  time_ms: 100",
+                ">",
+                "events <",
+                "  dhcp_event <",
+                "    duration_ms: 192",
+                "    error_code: 0",
+                "    if_name: \"wlan0\"",
+                "    state_transition: \"SomeState\"",
+                "  >",
+                "  time_ms: 200",
+                ">",
+                "events <",
+                "  default_network_event <",
+                "    network_id <",
+                "      network_id: 102",
+                "    >",
+                "    previous_network_id <",
+                "      network_id: 101",
+                "    >",
+                "    previous_network_ip_support: 1",
+                "    transport_types: 1",
+                "    transport_types: 2",
+                "    transport_types: 3",
+                "  >",
+                "  time_ms: 300",
+                ">",
+                "events <",
+                "  ip_provisioning_event <",
+                "    event_type: 1",
+                "    if_name: \"wlan0\"",
+                "    latency_ms: 5678",
+                "  >",
+                "  time_ms: 400",
+                ">",
+                "events <",
+                "  time_ms: 500",
+                "  validation_probe_event <",
+                "    latency_ms: 40730",
+                "    network_id <",
+                "      network_id: 120",
+                "    >",
+                "    probe_result: 204",
+                "    probe_type: 1",
+                "  >",
+                ">",
+                "events <",
+                "  apf_statistics <",
+                "    dropped_ras: 2",
+                "    duration_ms: 45000",
+                "    matching_ras: 2",
+                "    max_program_size: 2048",
+                "    parse_errors: 2",
+                "    program_updates: 4",
+                "    received_ras: 10",
+                "    zero_lifetime_ras: 1",
+                "  >",
+                "  time_ms: 600",
+                ">",
+                "events <",
+                "  ra_event <",
+                "    dnssl_lifetime: -1",
+                "    prefix_preferred_lifetime: 300",
+                "    prefix_valid_lifetime: 400",
+                "    rdnss_lifetime: 1000",
+                "    route_info_lifetime: -1",
+                "    router_lifetime: 2000",
+                "  >",
+                "  time_ms: 700",
+                ">",
+                "version: 2");
+
+        verifySerialization(want, getdump("flush"));
+    }
+
+    String getdump(String ... command) {
+        StringWriter buffer = new StringWriter();
+        PrintWriter writer = new PrintWriter(buffer);
+        mService.impl.dump(null, writer, command);
+        return buffer.toString();
+    }
+
+    List<ConnectivityMetricsEvent> verifyEvents(int n, int timeoutMs) throws Exception {
+        ArgumentCaptor<ConnectivityMetricsEvent> captor =
+                ArgumentCaptor.forClass(ConnectivityMetricsEvent.class);
+        verify(mMockService, timeout(timeoutMs).times(n)).logEvent(captor.capture());
+        return captor.getAllValues();
+    }
+
+    List<ConnectivityMetricsEvent> verifyEvents(int n) throws Exception {
+        return verifyEvents(n, 10);
+    }
+
+    static void verifySerialization(String want, String output) {
+        try {
+            byte[] got = Base64.decode(output, Base64.DEFAULT);
+            IpConnectivityLogClass.IpConnectivityLog log =
+                    new IpConnectivityLogClass.IpConnectivityLog();
+            MessageNano.mergeFrom(log, got);
+            assertEquals(want, log.toString());
+        } catch (Exception e) {
+            fail(e.toString());
+        }
+    }
+
+    static String joinLines(String ... elems) {
+        StringBuilder b = new StringBuilder();
+        for (String s : elems) {
+            b.append(s).append("\n");
+        }
+        return b.toString();
+    }
+
+    static ConnectivityMetricsEvent expectedEvent(int timestamp) {
+        return new ConnectivityMetricsEvent((long)timestamp, 0, 0, FAKE_EV);
+    }
+
+    /** Outer equality for ConnectivityMetricsEvent to avoid overriding equals() and hashCode(). */
+    static void assertEventsEqual(ConnectivityMetricsEvent expected, ConnectivityMetricsEvent got) {
+        assertEquals(expected.timestamp, got.timestamp);
+        assertEquals(expected.componentTag, got.componentTag);
+        assertEquals(expected.eventTag, got.eventTag);
+        assertEquals(expected.data, got.data);
+    }
+
+    static final Comparator<ConnectivityMetricsEvent> EVENT_COMPARATOR =
+        new Comparator<ConnectivityMetricsEvent>() {
+            @Override
+            public int compare(ConnectivityMetricsEvent ev1, ConnectivityMetricsEvent ev2) {
+                return (int) (ev1.timestamp - ev2.timestamp);
+            }
+        };
+}
diff --git a/tests/net/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/net/java/com/android/server/connectivity/LingerMonitorTest.java
new file mode 100644
index 0000000..77956be
--- /dev/null
+++ b/tests/net/java/com/android/server/connectivity/LingerMonitorTest.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2016, 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.connectivity;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkMisc;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.format.DateUtils;
+import com.android.internal.R;
+import com.android.server.ConnectivityService;
+import com.android.server.connectivity.NetworkNotificationManager;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+import junit.framework.TestCase;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.reset;
+
+public class LingerMonitorTest extends TestCase {
+    static final String CELLULAR = "CELLULAR";
+    static final String WIFI     = "WIFI";
+
+    static final long LOW_RATE_LIMIT = DateUtils.MINUTE_IN_MILLIS;
+    static final long HIGH_RATE_LIMIT = 0;
+
+    static final int LOW_DAILY_LIMIT = 2;
+    static final int HIGH_DAILY_LIMIT = 1000;
+
+    LingerMonitor mMonitor;
+
+    @Mock ConnectivityService mConnService;
+    @Mock Context mCtx;
+    @Mock NetworkMisc mMisc;
+    @Mock NetworkNotificationManager mNotifier;
+    @Mock Resources mResources;
+
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mCtx.getResources()).thenReturn(mResources);
+        when(mCtx.getPackageName()).thenReturn("com.android.server.connectivity");
+        when(mConnService.createNetworkMonitor(any(), any(), any(), any())).thenReturn(null);
+
+        mMonitor = new TestableLingerMonitor(mCtx, mNotifier, HIGH_DAILY_LIMIT, HIGH_RATE_LIMIT);
+    }
+
+    @SmallTest
+    public void testTransitions() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        NetworkAgentInfo nai1 = wifiNai(100);
+        NetworkAgentInfo nai2 = cellNai(101);
+
+        assertTrue(mMonitor.isNotificationEnabled(nai1, nai2));
+        assertFalse(mMonitor.isNotificationEnabled(nai2, nai1));
+    }
+
+    @SmallTest
+    public void testNotificationOnLinger() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNotification(from, to);
+    }
+
+    @SmallTest
+    public void testToastOnLinger() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyToast(from, to);
+    }
+
+    @SmallTest
+    public void testNotificationClearedAfterDisconnect() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNotification(from, to);
+
+        mMonitor.noteDisconnect(to);
+        verify(mNotifier, times(1)).clearNotification(100);
+    }
+
+    @SmallTest
+    public void testNotificationClearedAfterSwitchingBack() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNotification(from, to);
+
+        mMonitor.noteLingerDefaultNetwork(to, from);
+        verify(mNotifier, times(1)).clearNotification(100);
+    }
+
+    @SmallTest
+    public void testUniqueToast() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyToast(from, to);
+
+        mMonitor.noteLingerDefaultNetwork(to, from);
+        verify(mNotifier, times(1)).clearNotification(100);
+
+        reset(mNotifier);
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNoNotifications();
+    }
+
+    @SmallTest
+    public void testMultipleNotifications() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+        NetworkAgentInfo wifi1 = wifiNai(100);
+        NetworkAgentInfo wifi2 = wifiNai(101);
+        NetworkAgentInfo cell = cellNai(102);
+
+        mMonitor.noteLingerDefaultNetwork(wifi1, cell);
+        verifyNotification(wifi1, cell);
+
+        mMonitor.noteLingerDefaultNetwork(cell, wifi2);
+        verify(mNotifier, times(1)).clearNotification(100);
+
+        reset(mNotifier);
+        mMonitor.noteLingerDefaultNetwork(wifi2, cell);
+        verifyNotification(wifi2, cell);
+    }
+
+    @SmallTest
+    public void testRateLimiting() throws InterruptedException {
+        mMonitor = new TestableLingerMonitor(mCtx, mNotifier, HIGH_DAILY_LIMIT, LOW_RATE_LIMIT);
+
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+        NetworkAgentInfo wifi1 = wifiNai(100);
+        NetworkAgentInfo wifi2 = wifiNai(101);
+        NetworkAgentInfo wifi3 = wifiNai(102);
+        NetworkAgentInfo cell = cellNai(103);
+
+        mMonitor.noteLingerDefaultNetwork(wifi1, cell);
+        verifyNotification(wifi1, cell);
+        reset(mNotifier);
+
+        Thread.sleep(50);
+        mMonitor.noteLingerDefaultNetwork(cell, wifi2);
+        mMonitor.noteLingerDefaultNetwork(wifi2, cell);
+        verifyNoNotifications();
+
+        Thread.sleep(50);
+        mMonitor.noteLingerDefaultNetwork(cell, wifi3);
+        mMonitor.noteLingerDefaultNetwork(wifi3, cell);
+        verifyNoNotifications();
+    }
+
+    @SmallTest
+    public void testDailyLimiting() throws InterruptedException {
+        mMonitor = new TestableLingerMonitor(mCtx, mNotifier, LOW_DAILY_LIMIT, HIGH_RATE_LIMIT);
+
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+        NetworkAgentInfo wifi1 = wifiNai(100);
+        NetworkAgentInfo wifi2 = wifiNai(101);
+        NetworkAgentInfo wifi3 = wifiNai(102);
+        NetworkAgentInfo cell = cellNai(103);
+
+        mMonitor.noteLingerDefaultNetwork(wifi1, cell);
+        verifyNotification(wifi1, cell);
+        reset(mNotifier);
+
+        Thread.sleep(50);
+        mMonitor.noteLingerDefaultNetwork(cell, wifi2);
+        mMonitor.noteLingerDefaultNetwork(wifi2, cell);
+        verifyNotification(wifi2, cell);
+        reset(mNotifier);
+
+        Thread.sleep(50);
+        mMonitor.noteLingerDefaultNetwork(cell, wifi3);
+        mMonitor.noteLingerDefaultNetwork(wifi3, cell);
+        verifyNoNotifications();
+    }
+
+    @SmallTest
+    public void testUniqueNotification() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNotification(from, to);
+
+        mMonitor.noteLingerDefaultNetwork(to, from);
+        verify(mNotifier, times(1)).clearNotification(100);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNotification(from, to);
+    }
+
+    @SmallTest
+    public void testIgnoreNeverValidatedNetworks() {
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+        from.everValidated = false;
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNoNotifications();
+    }
+
+    @SmallTest
+    public void testIgnoreCurrentlyValidatedNetworks() {
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+        from.lastValidated = true;
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNoNotifications();
+    }
+
+    @SmallTest
+    public void testNoNotificationType() {
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+        setNotificationSwitch();
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNoNotifications();
+    }
+
+    @SmallTest
+    public void testNoTransitionToNotify() {
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NONE);
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNoNotifications();
+    }
+
+    @SmallTest
+    public void testDifferentTransitionToNotify() {
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+        setNotificationSwitch(transition(CELLULAR, WIFI));
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNoNotifications();
+    }
+
+    void setNotificationSwitch(String... transitions) {
+        when(mResources.getStringArray(R.array.config_networkNotifySwitches))
+                .thenReturn(transitions);
+    }
+
+    String transition(String from, String to) {
+        return from + "-" + to;
+    }
+
+    void setNotificationType(int type) {
+        when(mResources.getInteger(R.integer.config_networkNotifySwitchType)).thenReturn(type);
+    }
+
+    void verifyNoToast() {
+        verify(mNotifier, never()).showToast(any(), any());
+    }
+
+    void verifyNoNotification() {
+        verify(mNotifier, never())
+                .showNotification(anyInt(), any(), any(), any(), any(), anyBoolean());
+    }
+
+    void verifyNoNotifications() {
+        verifyNoToast();
+        verifyNoNotification();
+    }
+
+    void verifyToast(NetworkAgentInfo from, NetworkAgentInfo to) {
+        verifyNoNotification();
+        verify(mNotifier, times(1)).showToast(from, to);
+    }
+
+    void verifyNotification(NetworkAgentInfo from, NetworkAgentInfo to) {
+        verifyNoToast();
+        verify(mNotifier, times(1)).showNotification(eq(from.network.netId),
+                eq(NotificationType.NETWORK_SWITCH), eq(from), eq(to), any(), eq(true));
+    }
+
+    NetworkAgentInfo nai(int netId, int transport, int networkType, String networkTypeName) {
+        NetworkInfo info = new NetworkInfo(networkType, 0, networkTypeName, "");
+        NetworkCapabilities caps = new NetworkCapabilities();
+        caps.addCapability(0);
+        caps.addTransportType(transport);
+        NetworkAgentInfo nai = new NetworkAgentInfo(null, null, new Network(netId), info, null,
+                caps, 50, mCtx, null, mMisc, null, mConnService);
+        nai.everValidated = true;
+        return nai;
+    }
+
+    NetworkAgentInfo wifiNai(int netId) {
+        return nai(netId, NetworkCapabilities.TRANSPORT_WIFI,
+                ConnectivityManager.TYPE_WIFI, WIFI);
+    }
+
+    NetworkAgentInfo cellNai(int netId) {
+        return nai(netId, NetworkCapabilities.TRANSPORT_CELLULAR,
+                ConnectivityManager.TYPE_MOBILE, CELLULAR);
+    }
+
+    public static class TestableLingerMonitor extends LingerMonitor {
+        public TestableLingerMonitor(Context c, NetworkNotificationManager n, int l, long r) {
+            super(c, n, l, r);
+        }
+        @Override protected PendingIntent createNotificationIntent() {
+            return null;
+        }
+    }
+}
diff --git a/tests/net/java/com/android/server/connectivity/MetricsTestUtil.java b/tests/net/java/com/android/server/connectivity/MetricsTestUtil.java
new file mode 100644
index 0000000..e201012
--- /dev/null
+++ b/tests/net/java/com/android/server/connectivity/MetricsTestUtil.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2016 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.connectivity;
+
+import android.net.ConnectivityMetricsEvent;
+import android.net.ConnectivityMetricsLogger;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+abstract public class MetricsTestUtil {
+    private MetricsTestUtil() {
+    }
+
+    static ConnectivityMetricsEvent ipEv(Parcelable p) {
+        return ev(ConnectivityMetricsLogger.COMPONENT_TAG_CONNECTIVITY, p);
+    }
+
+    static ConnectivityMetricsEvent telephonyEv() {
+        return ev(ConnectivityMetricsLogger.COMPONENT_TAG_TELEPHONY, new Bundle());
+    }
+
+    static ConnectivityMetricsEvent ev(int tag, Parcelable p) {
+        return new ConnectivityMetricsEvent(1L, tag, 0, p);
+    }
+
+    // Utiliy interface for describing the content of a Parcel. This relies on
+    // the implementation defails of Parcelable and on the fact that the fully
+    // qualified Parcelable class names are written as string in the Parcels.
+    interface ParcelField {
+        void write(Parcel p);
+    }
+
+    static ConnectivityMetricsEvent describeIpEvent(ParcelField... fs) {
+        Parcel p = Parcel.obtain();
+        for (ParcelField f : fs) {
+            f.write(p);
+        }
+        p.setDataPosition(0);
+        return ipEv(p.readParcelable(ClassLoader.getSystemClassLoader()));
+    }
+
+    static ParcelField aType(Class<?> c) {
+        return new ParcelField() {
+            public void write(Parcel p) {
+                p.writeString(c.getName());
+            }
+        };
+    }
+
+    static ParcelField aBool(boolean b) {
+        return aByte((byte) (b ? 1 : 0));
+    }
+
+    static ParcelField aByte(byte b) {
+        return new ParcelField() {
+            public void write(Parcel p) {
+                p.writeByte(b);
+            }
+        };
+    }
+
+    static ParcelField anInt(int i) {
+        return new ParcelField() {
+            public void write(Parcel p) {
+                p.writeInt(i);
+            }
+        };
+    }
+
+    static ParcelField aLong(long l) {
+        return new ParcelField() {
+            public void write(Parcel p) {
+                p.writeLong(l);
+            }
+        };
+    }
+
+    static ParcelField aString(String s) {
+        return new ParcelField() {
+            public void write(Parcel p) {
+                p.writeString(s);
+            }
+        };
+    }
+
+    static ParcelField aByteArray(byte... ary) {
+        return new ParcelField() {
+            public void write(Parcel p) {
+                p.writeByteArray(ary);
+            }
+        };
+    }
+
+    static ParcelField anIntArray(int... ary) {
+        return new ParcelField() {
+            public void write(Parcel p) {
+                p.writeIntArray(ary);
+            }
+        };
+    }
+
+    static byte b(int i) {
+        return (byte) i;
+    }
+}
diff --git a/tests/net/java/com/android/server/connectivity/NetdEventListenerServiceTest.java b/tests/net/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
new file mode 100644
index 0000000..2bb62bb
--- /dev/null
+++ b/tests/net/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2016, 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.connectivity;
+
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.metrics.DnsEvent;
+import android.net.metrics.INetdEventListener;
+import android.net.metrics.IpConnectivityLog;
+import android.os.RemoteException;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+import org.junit.Before;
+import org.junit.Test;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.OptionalInt;
+import java.util.stream.IntStream;
+
+public class NetdEventListenerServiceTest extends TestCase {
+
+    // TODO: read from NetdEventListenerService after this constant is read from system property
+    static final int BATCH_SIZE = 100;
+    static final int EVENT_TYPE = INetdEventListener.EVENT_GETADDRINFO;
+    // TODO: read from INetdEventListener
+    static final int RETURN_CODE = 1;
+
+    static final byte[] EVENT_TYPES  = new byte[BATCH_SIZE];
+    static final byte[] RETURN_CODES = new byte[BATCH_SIZE];
+    static final int[] LATENCIES     = new int[BATCH_SIZE];
+    static {
+        for (int i = 0; i < BATCH_SIZE; i++) {
+            EVENT_TYPES[i] = EVENT_TYPE;
+            RETURN_CODES[i] = RETURN_CODE;
+            LATENCIES[i] = i;
+        }
+    }
+
+    NetdEventListenerService mNetdEventListenerService;
+
+    @Mock ConnectivityManager mCm;
+    @Mock IpConnectivityLog mLog;
+    ArgumentCaptor<NetworkCallback> mCallbackCaptor;
+    ArgumentCaptor<DnsEvent> mEvCaptor;
+
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mCallbackCaptor = ArgumentCaptor.forClass(NetworkCallback.class);
+        mEvCaptor = ArgumentCaptor.forClass(DnsEvent.class);
+        mNetdEventListenerService = new NetdEventListenerService(mCm, mLog);
+
+        verify(mCm, times(1)).registerNetworkCallback(any(), mCallbackCaptor.capture());
+    }
+
+    @SmallTest
+    public void testOneBatch() throws Exception {
+        log(105, LATENCIES);
+        log(106, Arrays.copyOf(LATENCIES, BATCH_SIZE - 1)); // one lookup short of a batch event
+
+        verifyLoggedEvents(new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES));
+
+        log(106, Arrays.copyOfRange(LATENCIES, BATCH_SIZE - 1, BATCH_SIZE));
+
+        mEvCaptor = ArgumentCaptor.forClass(DnsEvent.class); // reset argument captor
+        verifyLoggedEvents(
+            new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
+            new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES));
+    }
+
+    @SmallTest
+    public void testSeveralBatches() throws Exception {
+        log(105, LATENCIES);
+        log(106, LATENCIES);
+        log(105, LATENCIES);
+        log(107, LATENCIES);
+
+        verifyLoggedEvents(
+            new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
+            new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES),
+            new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
+            new DnsEvent(107, EVENT_TYPES, RETURN_CODES, LATENCIES));
+    }
+
+    @SmallTest
+    public void testBatchAndNetworkLost() throws Exception {
+        byte[] eventTypes = Arrays.copyOf(EVENT_TYPES, 20);
+        byte[] returnCodes = Arrays.copyOf(RETURN_CODES, 20);
+        int[] latencies = Arrays.copyOf(LATENCIES, 20);
+
+        log(105, LATENCIES);
+        log(105, latencies);
+        mCallbackCaptor.getValue().onLost(new Network(105));
+        log(105, LATENCIES);
+
+        verifyLoggedEvents(
+            new DnsEvent(105, eventTypes, returnCodes, latencies),
+            new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
+            new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES));
+    }
+
+    @SmallTest
+    public void testConcurrentBatchesAndDumps() throws Exception {
+        final long stop = System.currentTimeMillis() + 100;
+        final PrintWriter pw = new PrintWriter(new FileOutputStream("/dev/null"));
+        new Thread() {
+            public void run() {
+                while (System.currentTimeMillis() < stop) {
+                    mNetdEventListenerService.dump(pw);
+                }
+            }
+        }.start();
+
+        logAsync(105, LATENCIES);
+        logAsync(106, LATENCIES);
+        logAsync(107, LATENCIES);
+
+        verifyLoggedEvents(500,
+            new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
+            new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES),
+            new DnsEvent(107, EVENT_TYPES, RETURN_CODES, LATENCIES));
+    }
+
+    @SmallTest
+    public void testConcurrentBatchesAndNetworkLoss() throws Exception {
+        logAsync(105, LATENCIES);
+        Thread.sleep(10L);
+        // call onLost() asynchronously to logAsync's onDnsEvent() calls.
+        mCallbackCaptor.getValue().onLost(new Network(105));
+
+        // do not verify unpredictable batch
+        verify(mLog, timeout(500).times(1)).log(any());
+    }
+
+    void log(int netId, int[] latencies) {
+        try {
+            for (int l : latencies) {
+                mNetdEventListenerService.onDnsEvent(netId, EVENT_TYPE, RETURN_CODE, l, null, null,
+                        0, 0);
+            }
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    void logAsync(int netId, int[] latencies) {
+        new Thread() {
+            public void run() {
+                log(netId, latencies);
+            }
+        }.start();
+    }
+
+    void verifyLoggedEvents(DnsEvent... expected) {
+        verifyLoggedEvents(0, expected);
+    }
+
+    void verifyLoggedEvents(int wait, DnsEvent... expectedEvents) {
+        verify(mLog, timeout(wait).times(expectedEvents.length)).log(mEvCaptor.capture());
+        for (DnsEvent got : mEvCaptor.getAllValues()) {
+            OptionalInt index = IntStream.range(0, expectedEvents.length)
+                    .filter(i -> eventsEqual(expectedEvents[i], got))
+                    .findFirst();
+            // Don't match same expected event more than once.
+            index.ifPresent(i -> expectedEvents[i] = null);
+            assertTrue(index.isPresent());
+        }
+    }
+
+    /** equality function for DnsEvent to avoid overriding equals() and hashCode(). */
+    static boolean eventsEqual(DnsEvent expected, DnsEvent got) {
+        return (expected == got) || ((expected != null) && (got != null)
+                && (expected.netId == got.netId)
+                && Arrays.equals(expected.eventTypes, got.eventTypes)
+                && Arrays.equals(expected.returnCodes, got.returnCodes)
+                && Arrays.equals(expected.latenciesMs, got.latenciesMs));
+    }
+}
diff --git a/tests/net/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/tests/net/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
new file mode 100644
index 0000000..813e928
--- /dev/null
+++ b/tests/net/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2016 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.connectivity;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.telephony.TelephonyManager;
+import android.test.suitebuilder.annotation.SmallTest;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import junit.framework.TestCase;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.*;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class NetworkNotificationManagerTest extends TestCase {
+
+    static final String NOTIFICATION_ID = NetworkNotificationManager.NOTIFICATION_ID;
+
+    static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities();
+    static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities();
+    static {
+        CELL_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+        CELL_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+
+        WIFI_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
+        WIFI_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+    }
+
+    @Mock Context mCtx;
+    @Mock Resources mResources;
+    @Mock PackageManager mPm;
+    @Mock TelephonyManager mTelephonyManager;
+    @Mock NotificationManager mNotificationManager;
+    @Mock NetworkAgentInfo mWifiNai;
+    @Mock NetworkAgentInfo mCellNai;
+    @Mock NetworkInfo mNetworkInfo;
+    ArgumentCaptor<Notification> mCaptor;
+
+    NetworkNotificationManager mManager;
+
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mCaptor = ArgumentCaptor.forClass(Notification.class);
+        mWifiNai.networkCapabilities = WIFI_CAPABILITIES;
+        mWifiNai.networkInfo = mNetworkInfo;
+        mCellNai.networkCapabilities = CELL_CAPABILITIES;
+        mCellNai.networkInfo = mNetworkInfo;
+        when(mCtx.getResources()).thenReturn(mResources);
+        when(mCtx.getPackageManager()).thenReturn(mPm);
+        when(mCtx.getApplicationInfo()).thenReturn(new ApplicationInfo());
+        when(mResources.getColor(anyInt(), any())).thenReturn(0xFF607D8B);
+
+        mManager = new NetworkNotificationManager(mCtx, mTelephonyManager, mNotificationManager);
+    }
+
+    @SmallTest
+    public void testNotificationsShownAndCleared() {
+        final int NETWORK_ID_BASE = 100;
+        List<NotificationType> types = Arrays.asList(NotificationType.values());
+        List<Integer> ids = new ArrayList<>(types.size());
+        for (int i = 0; i < ids.size(); i++) {
+            ids.add(NETWORK_ID_BASE + i);
+        }
+        Collections.shuffle(ids);
+        Collections.shuffle(types);
+
+        for (int i = 0; i < ids.size(); i++) {
+            mManager.showNotification(ids.get(i), types.get(i), mWifiNai, mCellNai, null, false);
+        }
+
+        Collections.shuffle(ids);
+        for (int i = 0; i < ids.size(); i++) {
+            mManager.clearNotification(ids.get(i));
+        }
+
+        for (int i = 0; i < ids.size(); i++) {
+            final int expectedId = NETWORK_ID_BASE + i;
+            verify(mNotificationManager, times(1))
+                    .notifyAsUser(eq(NOTIFICATION_ID), eq(expectedId), any(), any());
+            verify(mNotificationManager, times(1))
+                    .cancelAsUser(eq(NOTIFICATION_ID), eq(expectedId), any());
+        }
+    }
+
+    @SmallTest
+    public void testNoInternetNotificationsNotShownForCellular() {
+        mManager.showNotification(100, NO_INTERNET, mCellNai, mWifiNai, null, false);
+        mManager.showNotification(101, LOST_INTERNET, mCellNai, mWifiNai, null, false);
+
+        verify(mNotificationManager, never()).notifyAsUser(any(), anyInt(), any(), any());
+
+        mManager.showNotification(102, NO_INTERNET, mWifiNai, mCellNai, null, false);
+
+        verify(mNotificationManager, times(1))
+                .notifyAsUser(eq(NOTIFICATION_ID), eq(102), any(), any());
+    }
+
+    @SmallTest
+    public void testNotificationsNotShownIfNoInternetCapability() {
+        mWifiNai.networkCapabilities = new NetworkCapabilities();
+        mWifiNai.networkCapabilities .addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
+        mManager.showNotification(102, NO_INTERNET, mWifiNai, mCellNai, null, false);
+        mManager.showNotification(103, LOST_INTERNET, mWifiNai, mCellNai, null, false);
+        mManager.showNotification(104, NETWORK_SWITCH, mWifiNai, mCellNai, null, false);
+
+        verify(mNotificationManager, never()).notifyAsUser(any(), anyInt(), any(), any());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java
similarity index 77%
rename from services/tests/servicestests/src/com/android/server/connectivity/VpnTest.java
rename to tests/net/java/com/android/server/connectivity/VpnTest.java
index 5d8b843..b51b277 100644
--- a/services/tests/servicestests/src/com/android/server/connectivity/VpnTest.java
+++ b/tests/net/java/com/android/server/connectivity/VpnTest.java
@@ -25,9 +25,11 @@
 
 import android.annotation.UserIdInt;
 import android.app.AppOpsManager;
+import android.app.NotificationManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
+import android.net.NetworkInfo.DetailedState;
 import android.net.UidRange;
 import android.os.INetworkManagementService;
 import android.os.Looper;
@@ -43,6 +45,8 @@
 import java.util.Map;
 import java.util.Set;
 
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -88,14 +92,18 @@
     @Mock private PackageManager mPackageManager;
     @Mock private INetworkManagementService mNetService;
     @Mock private AppOpsManager mAppOps;
+    @Mock private NotificationManager mNotificationManager;
 
     @Override
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
         setMockedPackages(mPackages);
+        when(mContext.getPackageName()).thenReturn(Vpn.class.getPackage().getName());
         when(mContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mUserManager);
         when(mContext.getSystemService(eq(Context.APP_OPS_SERVICE))).thenReturn(mAppOps);
+        when(mContext.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
+                .thenReturn(mNotificationManager);
         doNothing().when(mNetService).registerObserver(any());
     }
 
@@ -103,7 +111,7 @@
     public void testRestrictedProfilesAreAddedToVpn() {
         setMockedUsers(primaryUser, secondaryUser, restrictedProfileA, restrictedProfileB);
 
-        final Vpn vpn = new MockVpn(primaryUser.id);
+        final Vpn vpn = spyVpn(primaryUser.id);
         final Set<UidRange> ranges = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
                 null, null);
 
@@ -117,7 +125,7 @@
     public void testManagedProfilesAreNotAddedToVpn() {
         setMockedUsers(primaryUser, managedProfileA);
 
-        final Vpn vpn = new MockVpn(primaryUser.id);
+        final Vpn vpn = spyVpn(primaryUser.id);
         final Set<UidRange> ranges = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
                 null, null);
 
@@ -130,7 +138,7 @@
     public void testAddUserToVpnOnlyAddsOneUser() {
         setMockedUsers(primaryUser, restrictedProfileA, managedProfileA);
 
-        final Vpn vpn = new MockVpn(primaryUser.id);
+        final Vpn vpn = spyVpn(primaryUser.id);
         final Set<UidRange> ranges = new ArraySet<>();
         vpn.addUserToRanges(ranges, primaryUser.id, null, null);
 
@@ -141,7 +149,7 @@
 
     @SmallTest
     public void testUidWhiteAndBlacklist() throws Exception {
-        final Vpn vpn = new MockVpn(primaryUser.id);
+        final Vpn vpn = spyVpn(primaryUser.id);
         final UidRange user = UidRange.createForUser(primaryUser.id);
         final String[] packages = {PKGS[0], PKGS[1], PKGS[2]};
 
@@ -166,15 +174,15 @@
 
     @SmallTest
     public void testLockdownChangingPackage() throws Exception {
-        final MockVpn vpn = new MockVpn(primaryUser.id);
+        final Vpn vpn = spyVpn(primaryUser.id);
         final UidRange user = UidRange.createForUser(primaryUser.id);
 
         // Default state.
-        vpn.assertUnblocked(user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
+        assertUnblocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
 
         // Set always-on without lockdown.
         assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false));
-        vpn.assertUnblocked(user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
+        assertUnblocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
 
         // Set always-on with lockdown.
         assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true));
@@ -182,8 +190,8 @@
             new UidRange(user.start, user.start + PKG_UIDS[1] - 1),
             new UidRange(user.start + PKG_UIDS[1] + 1, user.stop)
         }));
-        vpn.assertBlocked(user.start + PKG_UIDS[0], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
-        vpn.assertUnblocked(user.start + PKG_UIDS[1]);
+        assertBlocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
+        assertUnblocked(vpn, user.start + PKG_UIDS[1]);
 
         // Switch to another app.
         assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true));
@@ -195,13 +203,13 @@
             new UidRange(user.start, user.start + PKG_UIDS[3] - 1),
             new UidRange(user.start + PKG_UIDS[3] + 1, user.stop)
         }));
-        vpn.assertBlocked(user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2]);
-        vpn.assertUnblocked(user.start + PKG_UIDS[3]);
+        assertBlocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[1], user.start + PKG_UIDS[2]);
+        assertUnblocked(vpn, user.start + PKG_UIDS[3]);
     }
 
     @SmallTest
     public void testLockdownAddingAProfile() throws Exception {
-        final MockVpn vpn = new MockVpn(primaryUser.id);
+        final Vpn vpn = spyVpn(primaryUser.id);
         setMockedUsers(primaryUser);
 
         // Make a copy of the restricted profile, as we're going to mark it deleted halfway through.
@@ -220,7 +228,7 @@
         }));
 
         // Verify restricted user isn't affected at first.
-        vpn.assertUnblocked(profile.start + PKG_UIDS[0]);
+        assertUnblocked(vpn, profile.start + PKG_UIDS[0]);
 
         // Add the restricted user.
         setMockedUsers(primaryUser, tempProfile);
@@ -239,24 +247,53 @@
         }));
     }
 
+    @SmallTest
+    public void testNotificationShownForAlwaysOnApp() {
+        final Vpn vpn = spyVpn(primaryUser.id);
+        final InOrder order = inOrder(vpn);
+        setMockedUsers(primaryUser);
+
+        // Don't show a notification for regular disconnected states.
+        vpn.updateState(DetailedState.DISCONNECTED, TAG);
+        order.verify(vpn).updateAlwaysOnNotificationInternal(false);
+
+        // Start showing a notification for disconnected once always-on.
+        vpn.setAlwaysOnPackage(PKGS[0], false);
+        order.verify(vpn).updateAlwaysOnNotificationInternal(true);
+
+        // Stop showing the notification once connected.
+        vpn.updateState(DetailedState.CONNECTED, TAG);
+        order.verify(vpn).updateAlwaysOnNotificationInternal(false);
+
+        // Show the notification if we disconnect again.
+        vpn.updateState(DetailedState.DISCONNECTED, TAG);
+        order.verify(vpn).updateAlwaysOnNotificationInternal(true);
+
+        // Notification should be cleared after unsetting always-on package.
+        vpn.setAlwaysOnPackage(null, false);
+        order.verify(vpn).updateAlwaysOnNotificationInternal(false);
+    }
+
     /**
-     * A subclass of {@link Vpn} with some of the fields pre-mocked.
+     * Mock some methods of vpn object.
      */
-    private class MockVpn extends Vpn {
-        public MockVpn(@UserIdInt int userId) {
-            super(Looper.myLooper(), mContext, mNetService, userId);
-        }
+    private Vpn spyVpn(@UserIdInt int userId) {
+        final Vpn vpn = spy(new Vpn(Looper.myLooper(), mContext, mNetService, userId));
 
-        public void assertBlocked(int... uids) {
-            for (int uid : uids) {
-                assertTrue("Uid " + uid + " should be blocked", isBlockingUid(uid));
-            }
-        }
+        // Block calls to the NotificationManager or PendingIntent#getActivity.
+        doNothing().when(vpn).updateAlwaysOnNotificationInternal(anyBoolean());
+        return vpn;
+    }
 
-        public void assertUnblocked(int... uids) {
-            for (int uid : uids) {
-                assertFalse("Uid " + uid + " should not be blocked", isBlockingUid(uid));
-            }
+    private static void assertBlocked(Vpn vpn, int... uids) {
+        for (int uid : uids) {
+            assertTrue("Uid " + uid + " should be blocked", vpn.isBlockingUid(uid));
+        }
+    }
+
+    private static void assertUnblocked(Vpn vpn, int... uids) {
+        for (int uid : uids) {
+            assertFalse("Uid " + uid + " should not be blocked", vpn.isBlockingUid(uid));
         }
     }