Merge \"Make registerDefaultNetwork work on metered networks.\" into nyc-dev
am: 33cdc4d136  -s ours

Change-Id: Iacee53c01e7de163e677ab57bc2d7044f8aff952
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..847d82b 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,55 @@
     public final int legacyType;
 
     /**
+     * A NetworkRequest as used by the system 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_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.
+     *
+     *     - 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
+    };
+
+    /**
+     * 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 +107,7 @@
         networkCapabilities = new NetworkCapabilities(that.networkCapabilities);
         requestId = that.requestId;
         this.legacyType = that.legacyType;
+        this.type = that.type;
     }
 
     /**
@@ -90,7 +133,7 @@
             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);
         }
 
         /**
@@ -223,6 +266,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 +274,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 +283,27 @@
             }
         };
 
+    /**
+     * 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 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 type == Type.TRACK_DEFAULT || type == Type.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 +312,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/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 534b544..fb5b3f8 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -76,6 +76,7 @@
 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.os.Binder;
 import android.os.Build;
@@ -454,6 +455,8 @@
         }
     }
 
+    private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
+
     /**
      * Implements support for the legacy "one network per network type" model.
      *
@@ -487,8 +490,16 @@
          *
          * The actual lists are populated when we scan the network types that
          * are supported on this device.
+         *
+         * Threading model:
+         *  - addSupportedType() is only called in the constructor
+         *  - add(), update(), remove() are only called from the ConnectivityService handler thread.
+         *    They are therefore not thread-safe with respect to each other.
+         *  - getNetworkForType() can be called at any time on binder threads. It is synchronized
+         *    on mTypeLists to be thread-safe with respect to a concurrent remove call.
+         *  - dump is thread-safe with respect to concurrent add and remove calls.
          */
-        private ArrayList<NetworkAgentInfo> mTypeLists[];
+        private final ArrayList<NetworkAgentInfo> mTypeLists[];
 
         public LegacyTypeTracker() {
             mTypeLists = (ArrayList<NetworkAgentInfo>[])
@@ -508,11 +519,12 @@
         }
 
         public NetworkAgentInfo getNetworkForType(int type) {
-            if (isTypeSupported(type) && !mTypeLists[type].isEmpty()) {
-                return mTypeLists[type].get(0);
-            } else {
-                return null;
+            synchronized (mTypeLists) {
+                if (isTypeSupported(type) && !mTypeLists[type].isEmpty()) {
+                    return mTypeLists[type].get(0);
+                }
             }
+            return null;
         }
 
         private void maybeLogBroadcast(NetworkAgentInfo nai, DetailedState state, int type,
@@ -535,12 +547,13 @@
             if (list.contains(nai)) {
                 return;
             }
-
-            list.add(nai);
+            synchronized (mTypeLists) {
+                list.add(nai);
+            }
 
             // Send a broadcast if this is the first network of its type or if it's the default.
             final boolean isDefaultNetwork = isDefaultNetwork(nai);
-            if (list.size() == 1 || isDefaultNetwork) {
+            if ((list.size() == 1) || isDefaultNetwork) {
                 maybeLogBroadcast(nai, DetailedState.CONNECTED, type, isDefaultNetwork);
                 sendLegacyNetworkBroadcast(nai, DetailedState.CONNECTED, type);
             }
@@ -552,11 +565,12 @@
             if (list == null || list.isEmpty()) {
                 return;
             }
-
             final boolean wasFirstNetwork = list.get(0).equals(nai);
 
-            if (!list.remove(nai)) {
-                return;
+            synchronized (mTypeLists) {
+                if (!list.remove(nai)) {
+                    return;
+                }
             }
 
             final DetailedState state = DetailedState.DISCONNECTED;
@@ -591,8 +605,8 @@
             for (int type = 0; type < mTypeLists.length; type++) {
                 final ArrayList<NetworkAgentInfo> list = mTypeLists[type];
                 final boolean contains = (list != null && list.contains(nai));
-                final boolean isFirst = (list != null && list.size() > 0 && nai == list.get(0));
-                if (isFirst || (contains && isDefault)) {
+                final boolean isFirst = contains && (nai == list.get(0));
+                if (isFirst || contains && isDefault) {
                     maybeLogBroadcast(nai, state, type, isDefault);
                     sendLegacyNetworkBroadcast(nai, state, type);
                 }
@@ -617,10 +631,12 @@
             pw.println();
             pw.println("Current state:");
             pw.increaseIndent();
-            for (int type = 0; type < mTypeLists.length; type++) {
-                if (mTypeLists[type] == null|| mTypeLists[type].size() == 0) continue;
-                for (NetworkAgentInfo nai : mTypeLists[type]) {
-                    pw.println(type + " " + naiToString(nai));
+            synchronized (mTypeLists) {
+                for (int type = 0; type < mTypeLists.length; type++) {
+                    if (mTypeLists[type] == null || mTypeLists[type].isEmpty()) continue;
+                    for (NetworkAgentInfo nai : mTypeLists[type]) {
+                        pw.println(type + " " + naiToString(nai));
+                    }
                 }
             }
             pw.decreaseIndent();
@@ -640,8 +656,7 @@
         if (DBG) log("ConnectivityService starting up");
 
         mDefaultRequest = createInternetRequestForTransport(-1);
-        NetworkRequestInfo defaultNRI = new NetworkRequestInfo(null, mDefaultRequest,
-                new Binder(), NetworkRequestType.REQUEST);
+        NetworkRequestInfo defaultNRI = new NetworkRequestInfo(null, mDefaultRequest, new Binder());
         mNetworkRequests.put(mDefaultRequest, defaultNRI);
         mNetworkRequestInfoLogs.log("REGISTER " + defaultNRI);
 
@@ -792,7 +807,19 @@
         if (transportType > -1) {
             netCap.addTransportType(transportType);
         }
-        return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId());
+        return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId(),
+                NetworkRequest.Type.REQUEST);
+    }
+
+    // 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() {
@@ -805,7 +832,7 @@
 
         if (enable) {
             handleRegisterNetworkRequest(new NetworkRequestInfo(
-                    null, mDefaultMobileDataRequest, new Binder(), NetworkRequestType.REQUEST));
+                    null, mDefaultMobileDataRequest, new Binder()));
         } else {
             handleReleaseNetworkRequest(mDefaultMobileDataRequest, Process.SYSTEM_UID);
         }
@@ -1056,6 +1083,7 @@
         return nai != null ? nai.network : null;
     }
 
+    // Public because it's used by mLockdownTracker.
     public NetworkInfo getActiveNetworkInfoUnfiltered() {
         enforceAccessPermission();
         final int uid = Binder.getCallingUid();
@@ -1311,6 +1339,7 @@
      * desired
      * @return {@code true} on success, {@code false} on failure
      */
+    @Override
     public boolean requestRouteToHostAddress(int networkType, byte[] hostAddress) {
         enforceChangePermission();
         if (mProtectedNetworks.contains(networkType)) {
@@ -1519,6 +1548,7 @@
         mContext.enforceCallingOrSelfPermission(KeepaliveTracker.PERMISSION, "ConnectivityService");
     }
 
+    // Public because it's used by mLockdownTracker.
     public void sendConnectedBroadcast(NetworkInfo info) {
         enforceConnectivityInternalPermission();
         sendGeneralBroadcast(info, CONNECTIVITY_ACTION);
@@ -1877,8 +1907,8 @@
             pw.increaseIndent();
             pw.println("Requests:");
             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:");
@@ -1987,10 +2017,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) {
@@ -2159,9 +2185,11 @@
                             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) {
+                            setProvNotificationVisibleIntent(true, netId, NotificationType.SIGN_IN,
+                                    nai.networkInfo.getType(), nai.networkInfo.getExtraInfo(),
+                                    (PendingIntent)msg.obj, nai.networkMisc.explicitlySelected);
+                        }
                     }
                     break;
                 }
@@ -2179,7 +2207,7 @@
 
     private void linger(NetworkAgentInfo nai) {
         nai.lingering = true;
-        NetworkEvent.logEvent(nai.network.netId, NetworkEvent.NETWORK_LINGER);
+        logNetworkEvent(nai, NetworkEvent.NETWORK_LINGER);
         nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_LINGER);
         notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING);
     }
@@ -2193,7 +2221,7 @@
         nai.networkLingered.clear();
         if (!nai.lingering) return;
         nai.lingering = false;
-        NetworkEvent.logEvent(nai.network.netId, NetworkEvent.NETWORK_UNLINGER);
+        logNetworkEvent(nai, NetworkEvent.NETWORK_UNLINGER);
         if (VDBG) log("Canceling linger of " + nai.name());
         nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_CONNECTED);
     }
@@ -2205,7 +2233,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.isRequest()) continue;
                     NetworkAgentInfo nai = mNetworkForRequestId.get(nri.request.requestId);
                     ac.sendMessage(android.net.NetworkFactory.CMD_REQUEST_NETWORK,
                             (nai != null ? nai.getCurrentScore() : 0), 0, nri.request);
@@ -2240,7 +2268,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
@@ -2277,15 +2305,15 @@
                 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) {
+            if (nai.isSatisfyingRequest(mDefaultRequest.requestId)) {
                 removeDataActivityTracking(nai);
                 notifyLockdownVpn(nai);
                 requestNetworkTransitionWakelock(nai.name());
@@ -2346,7 +2374,7 @@
     private void handleRegisterNetworkRequest(NetworkRequestInfo nri) {
         mNetworkRequests.put(nri.request, nri);
         mNetworkRequestInfoLogs.log("REGISTER " + nri);
-        if (!nri.isRequest()) {
+        if (!nri.request.isRequest()) {
             for (NetworkAgentInfo network : mNetworkAgentInfos.values()) {
                 if (nri.request.networkCapabilities.hasSignalStrength() &&
                         network.satisfiesImmutableCapabilitiesOf(nri.request)) {
@@ -2355,7 +2383,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);
         }
     }
@@ -2376,8 +2404,8 @@
         for (NetworkRequestInfo nri : mNetworkRequests.values()) {
             // 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
@@ -2400,7 +2428,7 @@
                 if (DBG) log("Attempt to release unowned NetworkRequest " + request);
                 return;
             }
-            if (VDBG || (DBG && nri.isRequest())) log("releasing NetworkRequest " + request);
+            if (VDBG || (DBG && nri.request.isRequest())) log("releasing " + request);
             nri.unlinkDeathRecipient();
             mNetworkRequests.remove(request);
             synchronized (mUidToNetworkRequestCount) {
@@ -2416,19 +2444,18 @@
                 }
             }
             mNetworkRequestInfoLogs.log("RELEASE " + nri);
-            if (nri.isRequest()) {
+            if (nri.request.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 (nai.isSatisfyingRequest(nri.request.requestId)) {
+                        nai.removeRequest(nri.request.requestId);
                         if (VDBG) {
                             log(" Removing from current network " + nai.name() +
-                                    ", leaving " + nai.networkRequests.size() +
-                                    " requests.");
+                                    ", leaving " + nai.numNetworkRequests() + " requests.");
                         }
                         if (unneeded(nai)) {
                             if (DBG) log("no live requests for " + nai.name() + "; disconnecting");
@@ -2455,10 +2482,10 @@
                     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);
+                        for (int i = 0; i < nai.numNetworkRequests(); i++) {
+                            NetworkRequest otherRequest = nai.requestAt(i);
                             if (otherRequest.legacyType == nri.request.legacyType &&
-                                    isRequest(otherRequest)) {
+                                    otherRequest.isRequest()) {
                                 if (DBG) log(" still have other legacy request - leaving");
                                 doRemove = false;
                             }
@@ -2478,7 +2505,7 @@
                 // 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);
+                    nai.removeRequest(nri.request.requestId);
                     if (nri.request.networkCapabilities.hasSignalStrength() &&
                             nai.satisfiesImmutableCapabilitiesOf(nri.request)) {
                         updateSignalStrengthThresholds(nai, "RELEASE", nri.request);
@@ -2489,6 +2516,7 @@
         }
     }
 
+    @Override
     public void setAcceptUnvalidated(Network network, boolean accept, boolean always) {
         enforceConnectivityInternalPermission();
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_ACCEPT_UNVALIDATED,
@@ -2562,6 +2590,7 @@
 
         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);
     }
@@ -2671,6 +2700,7 @@
     }
 
     // javadoc from interface
+    @Override
     public int tether(String iface) {
         ConnectivityManager.enforceTetherChangePermission(mContext);
         if (isTetheringSupported()) {
@@ -2688,6 +2718,7 @@
     }
 
     // javadoc from interface
+    @Override
     public int untether(String iface) {
         ConnectivityManager.enforceTetherChangePermission(mContext);
 
@@ -2706,6 +2737,7 @@
     }
 
     // javadoc from interface
+    @Override
     public int getLastTetherError(String iface) {
         enforceTetherAccessPermission();
 
@@ -2717,6 +2749,7 @@
     }
 
     // TODO - proper iface API for selection by property, inspection, etc
+    @Override
     public String[] getTetherableUsbRegexs() {
         enforceTetherAccessPermission();
         if (isTetheringSupported()) {
@@ -2726,6 +2759,7 @@
         }
     }
 
+    @Override
     public String[] getTetherableWifiRegexs() {
         enforceTetherAccessPermission();
         if (isTetheringSupported()) {
@@ -2735,6 +2769,7 @@
         }
     }
 
+    @Override
     public String[] getTetherableBluetoothRegexs() {
         enforceTetherAccessPermission();
         if (isTetheringSupported()) {
@@ -2744,6 +2779,7 @@
         }
     }
 
+    @Override
     public int setUsbTethering(boolean enable) {
         ConnectivityManager.enforceTetherChangePermission(mContext);
         if (isTetheringSupported()) {
@@ -2755,21 +2791,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();
@@ -2828,12 +2868,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();
@@ -2878,6 +2920,7 @@
         }
     }
 
+    @Override
     public ProxyInfo getProxyForNetwork(Network network) {
         if (network == null) return getDefaultProxy();
         final ProxyInfo globalProxy = getGlobalProxy();
@@ -3793,31 +3836,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.
@@ -3831,27 +3855,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();
 
@@ -3872,16 +3895,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);
@@ -3894,29 +3907,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);
         }
     }
@@ -3966,18 +3958,21 @@
     @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);
         }
         ensureRequestableCapabilities(networkCapabilities);
@@ -3993,8 +3988,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));
@@ -4064,9 +4059,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));
@@ -4116,9 +4110,9 @@
         }
 
         NetworkRequest networkRequest = new NetworkRequest(
-                new NetworkCapabilities(networkCapabilities), TYPE_NONE, nextNetworkRequestId());
-        NetworkRequestInfo nri = new NetworkRequestInfo(messenger, networkRequest, binder,
-                NetworkRequestType.LISTEN);
+                new NetworkCapabilities(networkCapabilities), 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));
@@ -4134,9 +4128,9 @@
         }
 
         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));
@@ -4144,6 +4138,7 @@
 
     @Override
     public void releaseNetworkRequest(NetworkRequest networkRequest) {
+        ensureNetworkRequestHasType(networkRequest);
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_RELEASE_NETWORK_REQUEST, getCallingUid(),
                 0, networkRequest));
     }
@@ -4452,10 +4447,10 @@
     }
 
     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.isRequest()) continue;
             sendUpdatedScoreToFactories(nr, nai.getCurrentScore());
         }
     }
@@ -4545,12 +4540,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.isRequest()) continue;
+                loge("Dead network still had at least " + nr);
+                break;
+            }
         }
         nai.asyncChannel.disconnect();
     }
@@ -4632,7 +4629,7 @@
             // check if it satisfies the NetworkCapabilities
             if (VDBG) log("  checking if request is satisfied: " + nri.request);
             if (satisfies) {
-                if (!nri.isRequest()) {
+                if (!nri.request.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);
@@ -4651,7 +4648,7 @@
                     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.removeRequest(nri.request.requestId);
                         currentNetwork.networkLingered.add(nri.request);
                         affectedNetworks.add(currentNetwork);
                     } else {
@@ -4675,7 +4672,7 @@
                         oldDefaultNetwork = currentNetwork;
                     }
                 }
-            } 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
@@ -4687,12 +4684,12 @@
                     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()) {
+                    if (nri.request.isRequest()) {
                         Slog.wtf(TAG, "BUG: Removing request " + nri.request.requestId + " from " +
                                 newNetwork.name() +
                                 " without updating mNetworkForRequestId or factories!");
@@ -4792,9 +4789,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);
                 }
@@ -5053,7 +5050,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,
@@ -5073,8 +5070,8 @@
 
     protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType) {
         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);
             if (nri.mPendingIntent == null) {
@@ -5215,7 +5212,7 @@
         return new NetworkMonitor(context, handler, nai, defaultRequest);
     }
 
-    private static void logDefaultNetworkEvent(NetworkAgentInfo newNai, NetworkAgentInfo prevNai) {
+    private void logDefaultNetworkEvent(NetworkAgentInfo newNai, NetworkAgentInfo prevNai) {
         int newNetid = NETID_UNSET;
         int prevNetid = NETID_UNSET;
         int[] transports = new int[0];
@@ -5233,6 +5230,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/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index d487bd0..15b872d 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -162,11 +162,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>();
+    private final SparseArray<NetworkRequest> mNetworkRequests = new SparseArray<>();
     // 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>();
+    // How many of the satisfied requests are actual requests and not listens.
+    private int mNumRequestNetworkRequests = 0;
 
     public final Messenger messenger;
     public final AsyncChannel asyncChannel;
@@ -188,18 +190,63 @@
         networkMisc = misc;
     }
 
+    // Functions for manipulating the requests satisfied by this network.
+    //
+    // These functions must only called on ConnectivityService's main thread.
+
     /**
      * 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 && existing.isRequest()) mNumRequestNetworkRequests--;
+        mNetworkRequests.put(networkRequest.requestId, networkRequest);
+        if (networkRequest.isRequest()) mNumRequestNetworkRequests++;
         return true;
     }
 
+    /**
+     * Remove the specified request from this network.
+     */
+    public void removeRequest(int requestId) {
+        NetworkRequest existing = mNetworkRequests.get(requestId);
+        if (existing != null && existing.isRequest()) mNumRequestNetworkRequests--;
+        mNetworkRequests.remove(requestId);
+    }
+
+    /**
+     * 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 of any type currently satisfied by this network.
+     */
+    public int numNetworkRequests() {
+        return mNetworkRequests.size();
+    }
+
     // Does this network satisfy request?
     public boolean satisfies(NetworkRequest request) {
         return created &&
diff --git a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
index 4fae4a7..d424717 100644
--- a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
@@ -17,6 +17,7 @@
 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;
@@ -26,6 +27,7 @@
 
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
@@ -61,12 +63,15 @@
 import android.os.MessageQueue.IdleHandler;
 import android.os.Process;
 import android.os.SystemClock;
+import android.provider.Settings;
 import android.test.AndroidTestCase;
+import android.test.mock.MockContentResolver;
 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.FakeSettingsProvider;
 import com.android.internal.util.WakeupMessage;
 import com.android.server.connectivity.NetworkAgentInfo;
 import com.android.server.connectivity.NetworkMonitor;
@@ -75,6 +80,7 @@
 
 import java.net.InetAddress;
 import java.util.ArrayList;
+import java.util.Objects;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
@@ -96,6 +102,7 @@
     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
@@ -118,27 +125,24 @@
     }
 
     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 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) {
+        public Object getSystemService(String name) {
             if (name == Context.CONNECTIVITY_SERVICE) return mCm;
             return super.getSystemService(name);
         }
+
+        @Override
+        public ContentResolver getContentResolver() {
+            return mContentResolver;
+        }
     }
 
     /**
@@ -242,6 +246,9 @@
             mNetworkCapabilities = new NetworkCapabilities();
             mNetworkCapabilities.addTransportType(transport);
             switch (transport) {
+                case TRANSPORT_ETHERNET:
+                    mScore = 70;
+                    break;
                 case TRANSPORT_WIFI:
                     mScore = 60;
                     break;
@@ -303,6 +310,11 @@
             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);
@@ -641,7 +653,6 @@
         public void waitForIdle() {
             waitForIdle(TIMEOUT_MS);
         }
-
     }
 
     private interface Criteria {
@@ -694,14 +705,23 @@
         mCm.bindProcessToNetwork(null);
     }
 
+    public void tearDown() throws Exception {
+        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);
+                throw new IllegalStateException("Unknown transport " + transport);
         }
     }
 
@@ -911,8 +931,6 @@
         mWiFiNetworkAgent.adjustScore(11);
         waitFor(cv);
         verifyActiveNetwork(TRANSPORT_WIFI);
-        mCellNetworkAgent.disconnect();
-        mWiFiNetworkAgent.disconnect();
     }
 
     @LargeTest
@@ -984,8 +1002,6 @@
         assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
                 NET_CAPABILITY_VALIDATED));
         verifyActiveNetwork(TRANSPORT_WIFI);
-        mCellNetworkAgent.disconnect();
-        mWiFiNetworkAgent.disconnect();
     }
 
     @LargeTest
@@ -1012,8 +1028,6 @@
         assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
                 NET_CAPABILITY_VALIDATED));
         verifyActiveNetwork(TRANSPORT_WIFI);
-        mCellNetworkAgent.disconnect();
-        mWiFiNetworkAgent.disconnect();
     }
 
     enum CallbackState {
@@ -1029,59 +1043,79 @@
      * 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;
+        // 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 class CallbackInfo {
+            public final CallbackState state;
+            public final Network network;
+            public CallbackInfo(CallbackState s, Network n) { state = s; network = n; }
+            public String toString() { return String.format("%s (%s)", state, network); }
+            public boolean equals(Object o) {
+                if (!(o instanceof CallbackInfo)) return false;
+                CallbackInfo other = (CallbackInfo) o;
+                return state == other.state && Objects.equals(network, other.network);
+            }
+        }
+        private final LinkedBlockingQueue<CallbackInfo> mCallbacks = new LinkedBlockingQueue<>();
+
+        private void setLastCallback(CallbackState state, Network network) {
+            mCallbacks.offer(new CallbackInfo(state, network));
+        }
 
         public void onAvailable(Network network) {
-            assertEquals(CallbackState.NONE, mLastCallback);
-            mLastCallback = CallbackState.AVAILABLE;
-            mLastNetwork = network;
-            mConditionVariable.open();
+            setLastCallback(CallbackState.AVAILABLE, network);
         }
 
         public void onLosing(Network network, int maxMsToLive) {
-            assertEquals(CallbackState.NONE, mLastCallback);
-            mLastCallback = CallbackState.LOSING;
-            mLastNetwork = network;
-            mConditionVariable.open();
+            setLastCallback(CallbackState.LOSING, network);
         }
 
         public void onLost(Network network) {
-            assertEquals(CallbackState.NONE, mLastCallback);
-            mLastCallback = CallbackState.LOST;
-            mLastNetwork = network;
-            mConditionVariable.open();
-        }
-
-        void expectCallback(CallbackState state) {
-            expectCallback(state, null);
+            setLastCallback(CallbackState.LOST, network);
         }
 
         void expectCallback(CallbackState state, MockNetworkAgent mockAgent) {
-            waitFor(mConditionVariable);
-            assertEquals(state, mLastCallback);
-            if (mockAgent != null) {
-                assertEquals(mockAgent.getNetwork(), mLastNetwork);
+            CallbackInfo expected = new CallbackInfo(
+                    state,
+                    (mockAgent != null) ? mockAgent.getNetwork() : null);
+            try {
+                assertEquals("Unexpected callback:",
+                        expected, mCallbacks.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            } catch (InterruptedException e) {
+                fail("Did not receive expected " + expected + " after " + TIMEOUT_MS + "ms");
             }
-            mLastCallback = CallbackState.NONE;
-            mLastNetwork = null;
-            mConditionVariable.close();
         }
 
         void assertNoCallback() {
-            assertEquals(CallbackState.NONE, mLastCallback);
+            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();
         }
     }
 
     @LargeTest
     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);
 
@@ -1089,65 +1123,241 @@
         ConditionVariable cv = waitForConnectivityBroadcasts(1);
         mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
         mCellNetworkAgent.connect(false);
-        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE);
-        wifiNetworkCallback.assertNoCallback();
+        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();
-        wifiNetworkCallback.assertNoCallback();
-        cellNetworkCallback.assertNoCallback();
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
         assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
 
         cv = waitForConnectivityBroadcasts(2);
         mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
         mWiFiNetworkAgent.connect(false);
-        wifiNetworkCallback.expectCallback(CallbackState.AVAILABLE);
-        cellNetworkCallback.assertNoCallback();
+        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();
-        wifiNetworkCallback.expectCallback(CallbackState.LOST);
+        genericNetworkCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        wifiNetworkCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
         cellNetworkCallback.assertNoCallback();
         waitFor(cv);
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
 
         cv = waitForConnectivityBroadcasts(1);
         mCellNetworkAgent.disconnect();
-        cellNetworkCallback.expectCallback(CallbackState.LOST);
-        wifiNetworkCallback.assertNoCallback();
+        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);
-        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE);
-        wifiNetworkCallback.assertNoCallback();
+        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();
-        wifiNetworkCallback.assertNoCallback();
-        cellNetworkCallback.assertNoCallback();
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
         assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
 
         mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
         mWiFiNetworkAgent.connect(true);
-        wifiNetworkCallback.expectCallback(CallbackState.AVAILABLE);
-        cellNetworkCallback.expectCallback(CallbackState.LOSING);
+        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);
+            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();
-        wifiNetworkCallback.expectCallback(CallbackState.LOST);
-        cellNetworkCallback.assertNoCallback();
+        callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
 
-        mCellNetworkAgent.disconnect();
-        cellNetworkCallback.expectCallback(CallbackState.LOST);
-        wifiNetworkCallback.assertNoCallback();
+        // 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.LOSING, mCellNetworkAgent);
+        callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        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. In this case we do not linger cell. What happens is that
+        // when wifi connects, we don't linger because cell could potentially become the default
+        // network if it validated. Then, when wifi validates, we re-evaluate cell, see it has no
+        // requests, and tear it down because it's unneeded.
+        // TODO: can we linger in this case?
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        callback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+
+        // The current code has a bug: if a network is lingering, and we add and then remove a
+        // request from it, we forget that the network was lingering and tear it down immediately.
+        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);
+        mCm.unregisterNetworkCallback(noopCallback);
+        callback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+
+        mCm.unregisterNetworkCallback(callback);
+        mCm.unregisterNetworkCallback(defaultCallback);
     }
 
     private void tryNetworkFactoryRequests(int capability) throws Exception {
@@ -1314,7 +1524,7 @@
         mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
         mCellNetworkAgent.addCapability(NET_CAPABILITY_MMS);
         mCellNetworkAgent.connectWithoutInternet();
-        networkCallback.expectCallback(CallbackState.AVAILABLE);
+        networkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
         verifyActiveNetwork(TRANSPORT_WIFI);
         // Test releasing NetworkRequest disconnects cellular with MMS
         cv = mCellNetworkAgent.getDisconnectedCV();
@@ -1340,7 +1550,7 @@
         MockNetworkAgent mmsNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
         mmsNetworkAgent.addCapability(NET_CAPABILITY_MMS);
         mmsNetworkAgent.connectWithoutInternet();
-        networkCallback.expectCallback(CallbackState.AVAILABLE);
+        networkCallback.expectCallback(CallbackState.AVAILABLE, mmsNetworkAgent);
         verifyActiveNetwork(TRANSPORT_CELLULAR);
         // Test releasing MMS NetworkRequest does not disconnect main cellular NetworkAgent
         cv = mmsNetworkAgent.getDisconnectedCV();
@@ -1366,36 +1576,36 @@
         mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
         String firstRedirectUrl = "http://example.com/firstPath";
         mWiFiNetworkAgent.connectWithCaptivePortal(firstRedirectUrl);
-        captivePortalCallback.expectCallback(CallbackState.AVAILABLE);
+        captivePortalCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
         assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), firstRedirectUrl);
 
         // Take down network.
         // Expect onLost callback.
         mWiFiNetworkAgent.disconnect();
-        captivePortalCallback.expectCallback(CallbackState.LOST);
+        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);
+        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);
+        captivePortalCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
 
         // Expect NET_CAPABILITY_VALIDATED onAvailable callback.
-        validatedCallback.expectCallback(CallbackState.AVAILABLE);
+        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);
+        validatedCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
     }
 
     @SmallTest
@@ -1410,7 +1620,7 @@
             // do nothing - should get here
         }
 
-        assertTrue("NetworkReqeuest builder with MATCH_ALL_REQUESTS_NETWORK_SPECIFIER",
+        assertTrue("NetworkRequest builder with MATCH_ALL_REQUESTS_NETWORK_SPECIFIER",
                 execptionCalled);
 
         try {
@@ -1477,6 +1687,145 @@
         defaultNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
     }
 
+    @SmallTest
+    public void testRequestBenchmark() throws Exception {
+        // 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);
+
+        final int REGISTER_TIME_LIMIT_MS = 100;
+        long startTime = System.currentTimeMillis();
+        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(); }
+            };
+            mCm.registerNetworkCallback(request, callbacks[i]);
+        }
+        long timeTaken = System.currentTimeMillis() - startTime;
+        String msg = String.format("Register %d callbacks: %dms, acceptable %dms",
+                NUM_REQUESTS, timeTaken, REGISTER_TIME_LIMIT_MS);
+        Log.d(TAG, msg);
+        assertTrue(msg, timeTaken < REGISTER_TIME_LIMIT_MS);
+
+        final int CONNECT_TIME_LIMIT_MS = 30;
+        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);
+        startTime = System.currentTimeMillis();
+        if (!availableLatch.await(CONNECT_TIME_LIMIT_MS, TimeUnit.MILLISECONDS)) {
+            fail(String.format("Only dispatched %d/%d onAvailable callbacks in %dms",
+                    NUM_REQUESTS - availableLatch.getCount(), NUM_REQUESTS,
+                    CONNECT_TIME_LIMIT_MS));
+        }
+        timeTaken = System.currentTimeMillis() - startTime;
+        Log.d(TAG, String.format("Connect, %d callbacks: %dms, acceptable %dms",
+                NUM_REQUESTS, timeTaken, CONNECT_TIME_LIMIT_MS));
+
+        final int SWITCH_TIME_LIMIT_MS = 30;
+        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);
+        startTime = System.currentTimeMillis();
+        if (!losingLatch.await(SWITCH_TIME_LIMIT_MS, TimeUnit.MILLISECONDS)) {
+            fail(String.format("Only dispatched %d/%d onLosing callbacks in %dms",
+                    NUM_REQUESTS - losingLatch.getCount(), NUM_REQUESTS, SWITCH_TIME_LIMIT_MS));
+        }
+        timeTaken = System.currentTimeMillis() - startTime;
+        Log.d(TAG, String.format("Linger, %d callbacks: %dms, acceptable %dms",
+                NUM_REQUESTS, timeTaken, SWITCH_TIME_LIMIT_MS));
+
+        final int UNREGISTER_TIME_LIMIT_MS = 10;
+        startTime = System.currentTimeMillis();
+        for (int i = 0; i < NUM_REQUESTS; i++) {
+            mCm.unregisterNetworkCallback(callbacks[i]);
+        }
+        timeTaken = System.currentTimeMillis() - startTime;
+        msg = String.format("Unregister %d callbacks: %dms, acceptable %dms",
+                NUM_REQUESTS, timeTaken, UNREGISTER_TIME_LIMIT_MS);
+        Log.d(TAG, msg);
+        assertTrue(msg, timeTaken < UNREGISTER_TIME_LIMIT_MS);
+    }
+
+    @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);
+        Settings.Global.putInt(cr, Settings.Global.MOBILE_DATA_ALWAYS_ON, 1);
+        mService.updateMobileDataAlwaysOn();
+        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);
+        Settings.Global.putInt(cr, Settings.Global.MOBILE_DATA_ALWAYS_ON, 0);
+        mService.updateMobileDataAlwaysOn();
+        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();
+    }
+
     private static class TestKeepaliveCallback extends PacketKeepaliveCallback {
 
         public static enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR };
diff --git a/services/tests/servicestests/src/com/android/server/connectivity/DnsEventListenerServiceTest.java b/services/tests/servicestests/src/com/android/server/connectivity/DnsEventListenerServiceTest.java
new file mode 100644
index 0000000..033b2c9
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/connectivity/DnsEventListenerServiceTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.IDnsEventListener;
+import android.net.metrics.IpConnectivityLog;
+
+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 DnsEventListenerServiceTest extends TestCase {
+
+    // TODO: read from DnsEventListenerService after this constant is read from system property
+    static final int BATCH_SIZE = 100;
+    static final int EVENT_TYPE = IDnsEventListener.EVENT_GETADDRINFO;
+    // TODO: read from IDnsEventListener
+    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;
+        }
+    }
+
+    DnsEventListenerService mDnsService;
+
+    @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);
+        mDnsService = new DnsEventListenerService(mCm, mLog);
+
+        verify(mCm, times(1)).registerNetworkCallback(any(), mCallbackCaptor.capture());
+    }
+
+    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));
+    }
+
+    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));
+    }
+
+    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));
+    }
+
+    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) {
+                    mDnsService.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));
+    }
+
+    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) {
+        for (int l : latencies) {
+            mDnsService.onDnsEvent(netId, EVENT_TYPE, RETURN_CODE, l);
+        }
+    }
+
+    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));
+    }
+}