OSDN Git Service

Fix missing onLost NetworkCallbacks when network loses capability
authorPaul Jensen <pauljensen@google.com>
Wed, 1 Jul 2015 18:16:32 +0000 (14:16 -0400)
committerPaul Jensen <pauljensen@google.com>
Tue, 28 Jul 2015 16:19:32 +0000 (12:19 -0400)
If a network no longer satisfies a NetworkRequest, send the onLost
NetworkCallback.  If it was a real request (not listen) then update
the NetworkFactories.

To test this change I created a little infrastructure to fake
different Internet connectivity probe results during tests.  This
allowed me to rewrite some of ConnectivityServiceTest's logic for
validating networks.  This brought to light a couple issues that
I had to address to keep tests passing:
1. testUnlingeringDoesNotValidate was relying on a bad side-effect
   of my old method of ConnectivityServiceTest's logic for
   validating networks, so I rewrote the test.
2. ConnectivityService was not sending out NetworkCallbacks for
   WiFi when Cellular was validated.  I'm including a fix for this
   in this CL also.

Bug:22220234
Change-Id: I29314f38189817f8b2561a213c4f9e8522696663

core/java/android/net/NetworkCapabilities.java
services/core/java/com/android/server/ConnectivityService.java
services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
services/core/java/com/android/server/connectivity/NetworkMonitor.java
services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java

index 514d24a..651fb35 100644 (file)
@@ -626,6 +626,7 @@ public final class NetworkCapabilities implements Parcelable {
                 case NET_CAPABILITY_TRUSTED:        capabilities += "TRUSTED"; break;
                 case NET_CAPABILITY_NOT_VPN:        capabilities += "NOT_VPN"; break;
                 case NET_CAPABILITY_VALIDATED:      capabilities += "VALIDATED"; break;
+                case NET_CAPABILITY_CAPTIVE_PORTAL: capabilities += "CAPTIVE_PORTAL"; break;
             }
             if (++i < types.length) capabilities += "&";
         }
index eb74ab0..6bc1a59 100644 (file)
@@ -2142,6 +2142,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
                 mDefaultInetConditionPublished = 0;
             }
             notifyIfacesChanged();
+            // TODO - we shouldn't send CALLBACK_LOST to requests that can be satisfied
+            // by other networks that are already connected. Perhaps that can be done by
+            // sending all CALLBACK_LOST messages (for requests, not listens) at the end
+            // of rematchAllNetworksAndRequests
             notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOST);
             nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_DISCONNECTED);
             mNetworkAgentInfos.remove(msg.replyTo);
@@ -2250,7 +2254,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
                     //    is currently satisfying the request.  This is desirable when
                     //    cellular ends up validating but WiFi does not.
                     // 2. Unvalidated WiFi will not be reaped when validated cellular
-                    //    is currently satsifying the request.  This is desirable when
+                    //    is currently satisfying the request.  This is desirable when
                     //    WiFi ends up validating and out scoring cellular.
                     mNetworkForRequestId.get(nri.request.requestId).getCurrentScore() <
                             nai.getCurrentScoreAsValidated())) {
@@ -3799,10 +3803,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
 
         // TODO: Instead of passing mDefaultRequest, provide an API to determine whether a Network
         // satisfies mDefaultRequest.
-        NetworkAgentInfo nai = new NetworkAgentInfo(messenger, new AsyncChannel(),
+        final NetworkAgentInfo nai = new NetworkAgentInfo(messenger, new AsyncChannel(),
                 new Network(reserveNetId()), new NetworkInfo(networkInfo), new LinkProperties(
                 linkProperties), new NetworkCapabilities(networkCapabilities), currentScore,
-                mContext, mTrackerHandler, new NetworkMisc(networkMisc), mDefaultRequest);
+                mContext, mTrackerHandler, new NetworkMisc(networkMisc), mDefaultRequest, this);
         synchronized (this) {
             nai.networkMonitor.systemReady = mSystemReady;
         }
@@ -4196,8 +4200,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
         ArrayList<NetworkRequestInfo> addedRequests = new ArrayList<NetworkRequestInfo>();
         if (VDBG) log(" network has: " + newNetwork.networkCapabilities);
         for (NetworkRequestInfo nri : mNetworkRequests.values()) {
-            NetworkAgentInfo currentNetwork = mNetworkForRequestId.get(nri.request.requestId);
-            if (newNetwork == currentNetwork) {
+            final NetworkAgentInfo currentNetwork = mNetworkForRequestId.get(nri.request.requestId);
+            final boolean satisfies = newNetwork.satisfies(nri.request);
+            if (newNetwork == currentNetwork && satisfies) {
                 if (VDBG) {
                     log("Network " + newNetwork.name() + " was already satisfying" +
                             " request " + nri.request.requestId + ". No change.");
@@ -4208,7 +4213,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
 
             // check if it satisfies the NetworkCapabilities
             if (VDBG) log("  checking if request is satisfied: " + nri.request);
-            if (newNetwork.satisfies(nri.request)) {
+            if (satisfies) {
                 if (!nri.isRequest) {
                     // This is not a request, it's a callback listener.
                     // Add it to newNetwork regardless of score.
@@ -4251,6 +4256,37 @@ public class ConnectivityService extends IConnectivityManager.Stub
                         oldDefaultNetwork = currentNetwork;
                     }
                 }
+            } else if (newNetwork.networkRequests.get(nri.request.requestId) != null) {
+                // 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
+                // match "newNetwork" before this loop will encounter a "currentNetwork" with higher
+                // score than "newNetwork" and where "currentNetwork" no longer satisfies "nri".
+                // This means this code doesn't have to handle the case where "currentNetwork" no
+                // longer satisfies "nri" when "currentNetwork" does not equal "newNetwork".
+                if (DBG) {
+                    log("Network " + newNetwork.name() + " stopped satisfying" +
+                            " request " + nri.request.requestId);
+                }
+                newNetwork.networkRequests.remove(nri.request.requestId);
+                if (currentNetwork == newNetwork) {
+                    mNetworkForRequestId.remove(nri.request.requestId);
+                    sendUpdatedScoreToFactories(nri.request, 0);
+                } else {
+                    if (nri.isRequest == true) {
+                        Slog.wtf(TAG, "BUG: Removing request " + nri.request.requestId + " from " +
+                                newNetwork.name() +
+                                " without updating mNetworkForRequestId or factories!");
+                    }
+                }
+                // TODO: technically, sending CALLBACK_LOST here is
+                // incorrect if nri is a request (not a listen) and there
+                // is a replacement network currently connected that can
+                // satisfy it. However, the only capability that can both
+                // a) be requested and b) change is NET_CAPABILITY_TRUSTED,
+                // so this code is only incorrect for a network that loses
+                // the TRUSTED capability, which is a rare case.
+                callCallbackForRequest(nri, newNetwork, ConnectivityManager.CALLBACK_LOST);
             }
         }
         // Linger any networks that are no longer needed.
@@ -4269,41 +4305,41 @@ public class ConnectivityService extends IConnectivityManager.Stub
                 unlinger(nai);
             }
         }
-        if (keep) {
-            if (isNewDefault) {
-                // Notify system services that this network is up.
-                makeDefault(newNetwork);
-                synchronized (ConnectivityService.this) {
-                    // have a new default network, release the transition wakelock in
-                    // a second if it's held.  The second pause is to allow apps
-                    // to reconnect over the new network
-                    if (mNetTransitionWakeLock.isHeld()) {
-                        mHandler.sendMessageDelayed(mHandler.obtainMessage(
-                                EVENT_CLEAR_NET_TRANSITION_WAKELOCK,
-                                mNetTransitionWakeLockSerialNumber, 0),
-                                1000);
-                    }
+        if (isNewDefault) {
+            // Notify system services that this network is up.
+            makeDefault(newNetwork);
+            synchronized (ConnectivityService.this) {
+                // have a new default network, release the transition wakelock in
+                // a second if it's held.  The second pause is to allow apps
+                // to reconnect over the new network
+                if (mNetTransitionWakeLock.isHeld()) {
+                    mHandler.sendMessageDelayed(mHandler.obtainMessage(
+                            EVENT_CLEAR_NET_TRANSITION_WAKELOCK,
+                            mNetTransitionWakeLockSerialNumber, 0),
+                            1000);
                 }
             }
+        }
 
-            // do this after the default net is switched, but
-            // before LegacyTypeTracker sends legacy broadcasts
-            for (NetworkRequestInfo nri : addedRequests) notifyNetworkCallback(newNetwork, nri);
+        // do this after the default net is switched, but
+        // before LegacyTypeTracker sends legacy broadcasts
+        for (NetworkRequestInfo nri : addedRequests) notifyNetworkCallback(newNetwork, nri);
 
-            if (isNewDefault) {
-                // Maintain the illusion: since the legacy API only
-                // understands one network at a time, we must pretend
-                // that the current default network disconnected before
-                // the new one connected.
-                if (oldDefaultNetwork != null) {
-                    mLegacyTypeTracker.remove(oldDefaultNetwork.networkInfo.getType(),
-                                              oldDefaultNetwork, true);
-                }
-                mDefaultInetConditionPublished = newNetwork.lastValidated ? 100 : 0;
-                mLegacyTypeTracker.add(newNetwork.networkInfo.getType(), newNetwork);
-                notifyLockdownVpn(newNetwork);
+        if (isNewDefault) {
+            // Maintain the illusion: since the legacy API only
+            // understands one network at a time, we must pretend
+            // that the current default network disconnected before
+            // the new one connected.
+            if (oldDefaultNetwork != null) {
+                mLegacyTypeTracker.remove(oldDefaultNetwork.networkInfo.getType(),
+                                          oldDefaultNetwork, true);
             }
+            mDefaultInetConditionPublished = newNetwork.lastValidated ? 100 : 0;
+            mLegacyTypeTracker.add(newNetwork.networkInfo.getType(), newNetwork);
+            notifyLockdownVpn(newNetwork);
+        }
 
+        if (keep) {
             // Notify battery stats service about this network, both the normal
             // interface and any stacked links.
             // TODO: Avoid redoing this; this must only be done once when a network comes online.
@@ -4697,4 +4733,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
             }
         }
     }
+
+    @VisibleForTesting
+    public NetworkMonitor createNetworkMonitor(Context context, Handler handler,
+            NetworkAgentInfo nai, NetworkRequest defaultRequest) {
+        return new NetworkMonitor(context, handler, nai, defaultRequest);
+    }
+
 }
index 8a79430..39333f6 100644 (file)
@@ -30,6 +30,7 @@ import android.os.Messenger;
 import android.util.SparseArray;
 
 import com.android.internal.util.AsyncChannel;
+import com.android.server.ConnectivityService;
 import com.android.server.connectivity.NetworkMonitor;
 
 import java.util.ArrayList;
@@ -51,7 +52,7 @@ import java.util.Comparator;
 //    ConnectivityService will tell netd to create the network and immediately transition to
 //    state #3.
 // 3. registered, created, connected, unvalidated
-//    If this network can satsify the default NetworkRequest, then NetworkMonitor will
+//    If this network can satisfy the default NetworkRequest, then NetworkMonitor will
 //    probe for Internet connectivity.
 //    If this network cannot satisfy the default NetworkRequest, it will immediately be
 //    transitioned to state #4.
@@ -164,7 +165,7 @@ public class NetworkAgentInfo implements Comparable<NetworkAgentInfo> {
 
     public NetworkAgentInfo(Messenger messenger, AsyncChannel ac, Network net, NetworkInfo info,
             LinkProperties lp, NetworkCapabilities nc, int score, Context context, Handler handler,
-            NetworkMisc misc, NetworkRequest defaultRequest) {
+            NetworkMisc misc, NetworkRequest defaultRequest, ConnectivityService connService) {
         this.messenger = messenger;
         asyncChannel = ac;
         network = net;
@@ -172,7 +173,7 @@ public class NetworkAgentInfo implements Comparable<NetworkAgentInfo> {
         linkProperties = lp;
         networkCapabilities = nc;
         currentScore = score;
-        networkMonitor = new NetworkMonitor(context, handler, this, defaultRequest);
+        networkMonitor = connService.createNetworkMonitor(context, handler, this, defaultRequest);
         networkMisc = misc;
     }
 
index 2633939..08aa51c 100644 (file)
@@ -349,7 +349,7 @@ public class NetworkMonitor extends StateMachine {
     // Being in the ValidatedState State indicates a Network is:
     // - Successfully validated, or
     // - Wanted "as is" by the user, or
-    // - Does not satsify the default NetworkRequest and so validation has been skipped.
+    // - Does not satisfy the default NetworkRequest and so validation has been skipped.
     private class ValidatedState extends State {
         @Override
         public void enter() {
@@ -558,7 +558,7 @@ public class NetworkMonitor extends StateMachine {
 
     // Being in the LingeringState State indicates a Network's validated bit is true and it once
     // was the highest scoring Network satisfying a particular NetworkRequest, but since then
-    // another Network satsified the NetworkRequest with a higher score and hence this Network
+    // another Network satisfied the NetworkRequest with a higher score and hence this Network
     // is "lingered" for a fixed period of time before it is disconnected.  This period of time
     // allows apps to wrap up communication and allows for seamless reactivation if the other
     // higher scoring Network happens to disconnect.
@@ -633,7 +633,8 @@ public class NetworkMonitor extends StateMachine {
      * Do a URL fetch on a known server to see if we get the data we expect.
      * Returns HTTP response code.
      */
-    private int isCaptivePortal() {
+    @VisibleForTesting
+    protected int isCaptivePortal() {
         if (!mIsCaptivePortalCheckEnabled) return 204;
 
         HttpURLConnection urlConnection = null;
index cb9c6a7..3618e1a 100644 (file)
@@ -81,6 +81,7 @@ import android.test.suitebuilder.annotation.LargeTest;
 import android.util.Log;
 import android.util.LogPrinter;
 
+import com.android.server.connectivity.NetworkAgentInfo;
 import com.android.server.connectivity.NetworkMonitor;
 
 import org.mockito.ArgumentCaptor;
@@ -118,7 +119,7 @@ public class ConnectivityServiceTest extends AndroidTestCase {
     private INetworkPolicyManager mPolicyService;
 
     private BroadcastInterceptingContext mServiceContext;
-    private ConnectivityService mService;
+    private WrappedConnectivityService mService;
     private ConnectivityManager mCm;
     private MockNetworkAgent mWiFiNetworkAgent;
     private MockNetworkAgent mCellNetworkAgent;
@@ -148,6 +149,7 @@ public class ConnectivityServiceTest extends AndroidTestCase {
     }
 
     private class MockNetworkAgent {
+        private final WrappedNetworkMonitor mWrappedNetworkMonitor;
         private final NetworkInfo mNetworkInfo;
         private final NetworkCapabilities mNetworkCapabilities;
         private final Thread mThread;
@@ -172,6 +174,7 @@ public class ConnectivityServiceTest extends AndroidTestCase {
                     throw new UnsupportedOperationException("unimplemented network type");
             }
             final ConditionVariable initComplete = new ConditionVariable();
+            final ConditionVariable networkMonitorAvailable = mService.getNetworkMonitorCreatedCV();
             mThread = new Thread() {
                 public void run() {
                     Looper.prepare();
@@ -186,6 +189,8 @@ public class ConnectivityServiceTest extends AndroidTestCase {
             };
             mThread.start();
             waitFor(initComplete);
+            waitFor(networkMonitorAvailable);
+            mWrappedNetworkMonitor = mService.getLastCreatedWrappedNetworkMonitor();
         }
 
         public void adjustScore(int change) {
@@ -211,44 +216,46 @@ public class ConnectivityServiceTest extends AndroidTestCase {
             assertEquals(mNetworkInfo.getDetailedState(), DetailedState.IDLE);
             assertFalse(mNetworkCapabilities.hasCapability(NET_CAPABILITY_INTERNET));
 
-            // To pretend network is validated, we transition it to the CONNECTED state without
-            // NET_CAPABILITY_INTERNET so NetworkMonitor doesn't bother trying to validate and
-            // just rubber stamps it as validated.  Afterwards we add NET_CAPABILITY_INTERNET so
-            // the network can satisfy the default request.
             NetworkCallback callback = null;
             final ConditionVariable validatedCv = new ConditionVariable();
             if (validated) {
-                // If we connect a network without INTERNET capability, it'll get reaped.
-                // Prevent the reaping by adding a NetworkRequest.
+                mWrappedNetworkMonitor.gen204ProbeResult = 204;
                 NetworkRequest request = new NetworkRequest.Builder()
                         .addTransportType(mNetworkCapabilities.getTransportTypes()[0])
                         .build();
                 callback = new NetworkCallback() {
                     public void onCapabilitiesChanged(Network network,
                             NetworkCapabilities networkCapabilities) {
-                        if (networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED)) {
+                        if (network.equals(getNetwork()) &&
+                            networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED)) {
                             validatedCv.open();
                         }
                     }
                 };
-                mCm.requestNetwork(request, callback);
-            } else {
-                mNetworkCapabilities.addCapability(NET_CAPABILITY_INTERNET);
-                mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
+                mCm.registerNetworkCallback(request, callback);
             }
+            addCapability(NET_CAPABILITY_INTERNET);
 
             connectWithoutInternet();
 
             if (validated) {
                 // Wait for network to validate.
                 waitFor(validatedCv);
-                mNetworkCapabilities.addCapability(NET_CAPABILITY_INTERNET);
-                mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
+                mWrappedNetworkMonitor.gen204ProbeResult = 500;
             }
 
             if (callback != null) mCm.unregisterNetworkCallback(callback);
         }
 
+        public void connectWithCaptivePortal() {
+            mWrappedNetworkMonitor.gen204ProbeResult = 200;
+            connect(false);
+            waitFor(new Criteria() { public boolean get() {
+                NetworkCapabilities caps = mCm.getNetworkCapabilities(getNetwork());
+                return caps != null && caps.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL);} });
+            mWrappedNetworkMonitor.gen204ProbeResult = 500;
+        }
+
         public void disconnect() {
             mNetworkInfo.setDetailedState(DetailedState.DISCONNECTED, null, null);
             mNetworkAgent.sendNetworkInfo(mNetworkInfo);
@@ -261,14 +268,18 @@ public class ConnectivityServiceTest extends AndroidTestCase {
         public ConditionVariable getDisconnectedCV() {
             return mDisconnected;
         }
+
+        public WrappedNetworkMonitor getWrappedNetworkMonitor() {
+            return mWrappedNetworkMonitor;
+        }
     }
 
     private static class MockNetworkFactory extends NetworkFactory {
-        final ConditionVariable mNetworkStartedCV = new ConditionVariable();
-        final ConditionVariable mNetworkStoppedCV = new ConditionVariable();
-        final ConditionVariable mNetworkRequestedCV = new ConditionVariable();
-        final ConditionVariable mNetworkReleasedCV = new ConditionVariable();
-        final AtomicBoolean mNetworkStarted = new AtomicBoolean(false);
+        private final ConditionVariable mNetworkStartedCV = new ConditionVariable();
+        private final ConditionVariable mNetworkStoppedCV = new ConditionVariable();
+        private final ConditionVariable mNetworkRequestedCV = new ConditionVariable();
+        private final ConditionVariable mNetworkReleasedCV = new ConditionVariable();
+        private final AtomicBoolean mNetworkStarted = new AtomicBoolean(false);
 
         public MockNetworkFactory(Looper looper, Context context, String logTag,
                 NetworkCapabilities filter) {
@@ -328,7 +339,26 @@ public class ConnectivityServiceTest extends AndroidTestCase {
         }
     }
 
+    // NetworkMonitor implementation allowing overriding of Internet connectivity probe result.
+    private class WrappedNetworkMonitor extends NetworkMonitor {
+        // HTTP response code fed back to NetworkMonitor for Internet connectivity probe.
+        public int gen204ProbeResult = 500;
+
+        public WrappedNetworkMonitor(Context context, Handler handler,
+            NetworkAgentInfo networkAgentInfo, NetworkRequest defaultRequest) {
+            super(context, handler, networkAgentInfo, defaultRequest);
+        }
+
+        @Override
+        protected int isCaptivePortal() {
+            return gen204ProbeResult;
+        }
+    }
+
     private class WrappedConnectivityService extends ConnectivityService {
+        private final ConditionVariable mNetworkMonitorCreated = new ConditionVariable();
+        private WrappedNetworkMonitor mLastCreatedNetworkMonitor;
+
         public WrappedConnectivityService(Context context, INetworkManagementService netManager,
                 INetworkStatsService statsService, INetworkPolicyManager policyManager) {
             super(context, netManager, statsService, policyManager);
@@ -360,6 +390,25 @@ public class ConnectivityServiceTest extends AndroidTestCase {
                 return netId;
             }
         }
+
+        @Override
+        public NetworkMonitor createNetworkMonitor(Context context, Handler handler,
+                NetworkAgentInfo nai, NetworkRequest defaultRequest) {
+            final WrappedNetworkMonitor monitor = new WrappedNetworkMonitor(context, handler, nai,
+                    defaultRequest);
+            mLastCreatedNetworkMonitor = monitor;
+            mNetworkMonitorCreated.open();
+            return monitor;
+        }
+
+        public WrappedNetworkMonitor getLastCreatedWrappedNetworkMonitor() {
+            return mLastCreatedNetworkMonitor;
+        }
+
+        public ConditionVariable getNetworkMonitorCreatedCV() {
+            mNetworkMonitorCreated.close();
+            return mNetworkMonitorCreated;
+        }
     }
 
     private interface Criteria {
@@ -586,29 +635,29 @@ public class ConnectivityServiceTest extends AndroidTestCase {
 
     @LargeTest
     public void testUnlingeringDoesNotValidate() throws Exception {
-        // Test bringing up unvalidated cellular.
-        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        // Test bringing up unvalidated WiFi.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
         ConditionVariable cv = waitForConnectivityBroadcasts(1);
-        mCellNetworkAgent.connect(false);
+        mWiFiNetworkAgent.connect(false);
         waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
-        assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
                 NET_CAPABILITY_VALIDATED));
-        // Test bringing up validated WiFi.
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        // Test bringing up validated cellular.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
         cv = waitForConnectivityBroadcasts(2);
-        mWiFiNetworkAgent.connect(true);
+        mCellNetworkAgent.connect(true);
         waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_WIFI);
-        assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
                 NET_CAPABILITY_VALIDATED));
-        // Test WiFi disconnect.
+        // Test cellular disconnect.
         cv = waitForConnectivityBroadcasts(2);
-        mWiFiNetworkAgent.disconnect();
+        mCellNetworkAgent.disconnect();
         waitFor(cv);
-        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        verifyActiveNetwork(TRANSPORT_WIFI);
         // Unlingering a network should not cause it to be marked as validated.
-        assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
                 NET_CAPABILITY_VALIDATED));
     }
 
@@ -846,12 +895,8 @@ public class ConnectivityServiceTest extends AndroidTestCase {
 
         cellCv = cellNetworkCallback.getConditionVariable();
         wifiCv = wifiNetworkCallback.getConditionVariable();
-        // Our method for faking successful validation generates an additional callback, so wait
-        // for broadcast instead.
-        cv = waitForConnectivityBroadcasts(1);
         mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
         mCellNetworkAgent.connect(true);
-        waitFor(cv);
         waitFor(cellCv);
         assertEquals(CallbackState.AVAILABLE, cellNetworkCallback.getLastCallback());
         assertEquals(CallbackState.NONE, wifiNetworkCallback.getLastCallback());
@@ -868,12 +913,8 @@ public class ConnectivityServiceTest extends AndroidTestCase {
 
         cellCv = cellNetworkCallback.getConditionVariable();
         wifiCv = wifiNetworkCallback.getConditionVariable();
-        // Our method for faking successful validation generates an additional callback, so wait
-        // for broadcast instead.
-        cv = waitForConnectivityBroadcasts(1);
         mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
         mWiFiNetworkAgent.connect(true);
-        waitFor(cv);
         waitFor(wifiCv);
         assertEquals(CallbackState.AVAILABLE, wifiNetworkCallback.getLastCallback());
         waitFor(cellCv);
@@ -1075,6 +1116,63 @@ public class ConnectivityServiceTest extends AndroidTestCase {
         verifyActiveNetwork(TRANSPORT_CELLULAR);
     }
 
+    @LargeTest
+    public void testCaptivePortal() {
+        final TestNetworkCallback captivePortalCallback = new TestNetworkCallback();
+        final NetworkRequest captivePortalRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build();
+        mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback);
+
+        final TestNetworkCallback validatedCallback = new TestNetworkCallback();
+        final NetworkRequest validatedRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_VALIDATED).build();
+        mCm.registerNetworkCallback(validatedRequest, validatedCallback);
+        ConditionVariable validatedCv = validatedCallback.getConditionVariable();
+
+        // Bring up a network with a captive portal.
+        // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL.
+        ConditionVariable cv = captivePortalCallback.getConditionVariable();
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connectWithCaptivePortal();
+        waitFor(cv);
+        assertEquals(CallbackState.AVAILABLE, captivePortalCallback.getLastCallback());
+
+        // Take down network.
+        // Expect onLost callback.
+        cv = captivePortalCallback.getConditionVariable();
+        mWiFiNetworkAgent.disconnect();
+        waitFor(cv);
+        assertEquals(CallbackState.LOST, captivePortalCallback.getLastCallback());
+
+        // Bring up a network with a captive portal.
+        // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL.
+        cv = captivePortalCallback.getConditionVariable();
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connectWithCaptivePortal();
+        waitFor(cv);
+        assertEquals(CallbackState.AVAILABLE, captivePortalCallback.getLastCallback());
+
+        // Make captive portal disappear then revalidate.
+        // Expect onLost callback because network no longer provides NET_CAPABILITY_CAPTIVE_PORTAL.
+        cv = captivePortalCallback.getConditionVariable();
+        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 204;
+        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true);
+        waitFor(cv);
+        assertEquals(CallbackState.LOST, captivePortalCallback.getLastCallback());
+
+        // Expect NET_CAPABILITY_VALIDATED onAvailable callback.
+        waitFor(validatedCv);
+        assertEquals(CallbackState.AVAILABLE, validatedCallback.getLastCallback());
+
+        // Break network connectivity.
+        // Expect NET_CAPABILITY_VALIDATED onLost callback.
+        validatedCv = validatedCallback.getConditionVariable();
+        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 500;
+        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), false);
+        waitFor(validatedCv);
+        assertEquals(CallbackState.LOST, validatedCallback.getLastCallback());
+    }
+
 //    @Override
 //    public void tearDown() throws Exception {
 //        super.tearDown();