OSDN Git Service

Fallback to Cellular if WiFi fails to validate
authorPaul Jensen <pauljensen@google.com>
Thu, 25 Jun 2015 19:30:08 +0000 (15:30 -0400)
committerPaul Jensen <pauljensen@google.com>
Thu, 9 Jul 2015 14:26:17 +0000 (10:26 -0400)
Previously, once a network validated, for the purposes of comparing networks
to select the default network, we always considered it validated.
With this change if a network later fails to validate, we'll take this latest
validation result into account.  This means if WiFi and cellular are up
(e.g. if we recently switched from cellular->WiFi, and cellular is now
lingering) and both are validated, but for some reason WiFi fails a validation,
cellular will become the default network connection.

Bug:20896761
Change-Id: I858aa10c1aaec5cd9032067f960963409107bdb1

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 6878caf..d95b233 100644 (file)
@@ -2047,12 +2047,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
     // if it's awaiting captive portal login, or if validation failed), this
     // may trigger a re-evaluation of the network.
     private void unlinger(NetworkAgentInfo nai) {
+        nai.networkLingered.clear();
+        if (!nai.lingering) return;
         nai.lingering = false;
         if (VDBG) log("Canceling linger of " + nai.name());
-        // If network has never been validated, it cannot have been lingered, so don't bother
-        // needlessly triggering a re-evaluation.
-        if (!nai.everValidated) return;
-        nai.networkLingered.clear();
         nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_CONNECTED);
     }
 
@@ -2207,43 +2205,28 @@ public class ConnectivityService extends IConnectivityManager.Stub
     }
 
     // Is nai unneeded by all NetworkRequests (and should be disconnected)?
-    // For validated Networks this is simply whether it is satsifying any NetworkRequests.
-    // For unvalidated Networks this is whether it is satsifying any NetworkRequests or
-    // were it to become validated, would it have a chance of satisfying any NetworkRequests.
+    // This is whether it is satisfying any NetworkRequests or were it to become validated,
+    // would it have a chance of satisfying any NetworkRequests.
     private boolean unneeded(NetworkAgentInfo nai) {
         if (!nai.created || nai.isVPN() || nai.lingering) return false;
-        boolean unneeded = true;
-        if (nai.everValidated) {
-            for (int i = 0; i < nai.networkRequests.size() && unneeded; i++) {
-                final NetworkRequest nr = nai.networkRequests.valueAt(i);
-                try {
-                    if (isRequest(nr)) unneeded = false;
-                } catch (Exception e) {
-                    loge("Request " + nr + " not found in mNetworkRequests.");
-                    loge("  it came from request list  of " + nai.name());
-                }
-            }
-        } else {
-            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 ||
-                        // 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
-                        //    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
-                        //    WiFi ends up validating and out scoring cellular.
-                        mNetworkForRequestId.get(nri.request.requestId).getCurrentScore() <
-                                nai.getCurrentScoreAsValidated())) {
-                    unneeded = false;
-                    break;
-                }
+        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 ||
+                    // 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
+                    //    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
+                    //    WiFi ends up validating and out scoring cellular.
+                    mNetworkForRequestId.get(nri.request.requestId).getCurrentScore() <
+                            nai.getCurrentScoreAsValidated())) {
+                return false;
             }
         }
-        return unneeded;
+        return true;
     }
 
     private void handleReleaseNetworkRequest(NetworkRequest request, int callingUid) {
@@ -3997,29 +3980,29 @@ public class ConnectivityService extends IConnectivityManager.Stub
      * augmented with any stateful capabilities implied from {@code networkAgent}
      * (e.g., validated status and captive portal status).
      *
-     * @param networkAgent the network having its capabilities updated.
+     * @param nai the network having its capabilities updated.
      * @param networkCapabilities the new network capabilities.
      */
-    private void updateCapabilities(NetworkAgentInfo networkAgent,
-            NetworkCapabilities networkCapabilities) {
+    private void updateCapabilities(NetworkAgentInfo nai, NetworkCapabilities networkCapabilities) {
         // Don't modify caller's NetworkCapabilities.
         networkCapabilities = new NetworkCapabilities(networkCapabilities);
-        if (networkAgent.lastValidated) {
+        if (nai.lastValidated) {
             networkCapabilities.addCapability(NET_CAPABILITY_VALIDATED);
         } else {
             networkCapabilities.removeCapability(NET_CAPABILITY_VALIDATED);
         }
-        if (networkAgent.lastCaptivePortalDetected) {
+        if (nai.lastCaptivePortalDetected) {
             networkCapabilities.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
         } else {
             networkCapabilities.removeCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
         }
-        if (!Objects.equals(networkAgent.networkCapabilities, networkCapabilities)) {
-            synchronized (networkAgent) {
-                networkAgent.networkCapabilities = networkCapabilities;
+        if (!Objects.equals(nai.networkCapabilities, networkCapabilities)) {
+            final int oldScore = nai.getCurrentScore();
+            synchronized (nai) {
+                nai.networkCapabilities = networkCapabilities;
             }
-            rematchAllNetworksAndRequests(networkAgent, networkAgent.getCurrentScore());
-            notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_CAP_CHANGED);
+            rematchAllNetworksAndRequests(nai, oldScore);
+            notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
         }
     }
 
@@ -4249,9 +4232,17 @@ public class ConnectivityService extends IConnectivityManager.Stub
         }
         // Linger any networks that are no longer needed.
         for (NetworkAgentInfo nai : affectedNetworks) {
-            if (nai.everValidated && unneeded(nai)) {
+            if (nai.lingering) {
+                // Already lingered.  Nothing to do.  This can only happen if "nai" is in
+                // "affectedNetworks" twice.  The reasoning being that to get added to
+                // "affectedNetworks", "nai" must have been satisfying a NetworkRequest
+                // (i.e. not lingered) so it could have only been lingered by this loop.
+                // unneeded(nai) will be false and we'll call unlinger() below which would
+                // be bad, so handle it here.
+            } else if (unneeded(nai)) {
                 linger(nai);
             } else {
+                // Clear nai.networkLingered we might have added above.
                 unlinger(nai);
             }
         }
@@ -4285,7 +4276,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
                     mLegacyTypeTracker.remove(oldDefaultNetwork.networkInfo.getType(),
                                               oldDefaultNetwork, true);
                 }
-                mDefaultInetConditionPublished = newNetwork.everValidated ? 100 : 0;
+                mDefaultInetConditionPublished = newNetwork.lastValidated ? 100 : 0;
                 mLegacyTypeTracker.add(newNetwork.networkInfo.getType(), newNetwork);
                 notifyLockdownVpn(newNetwork);
             }
index 0a0c096..8a79430 100644 (file)
@@ -16,6 +16,8 @@
 
 package com.android.server.connectivity;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+
 import android.content.Context;
 import android.net.LinkProperties;
 import android.net.Network;
@@ -39,6 +41,62 @@ import java.util.Comparator;
  * AsyncChannel/messenger for reaching that NetworkAgent and lists of NetworkRequests
  * interested in using it.  Default sort order is descending by score.
  */
+// States of a network:
+// --------------------
+// 1. registered, uncreated, disconnected, unvalidated
+//    This state is entered when a NetworkFactory registers a NetworkAgent in any state except
+//    the CONNECTED state.
+// 2. registered, uncreated, connected, unvalidated
+//    This state is entered when a registered NetworkAgent transitions to the CONNECTED state
+//    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
+//    probe for Internet connectivity.
+//    If this network cannot satisfy the default NetworkRequest, it will immediately be
+//    transitioned to state #4.
+//    A network may remain in this state if NetworkMonitor fails to find Internet connectivity,
+//    for example:
+//    a. a captive portal is present, or
+//    b. a WiFi router whose Internet backhaul is down, or
+//    c. a wireless connection stops transfering packets temporarily (e.g. device is in elevator
+//       or tunnel) but does not disconnect from the AP/cell tower, or
+//    d. a stand-alone device offering a WiFi AP without an uplink for configuration purposes.
+// 4. registered, created, connected, validated
+//
+// The device's default network connection:
+// ----------------------------------------
+// Networks in states #3 and #4 may be used as a device's default network connection if they
+// satisfy the default NetworkRequest.
+// A network, that satisfies the default NetworkRequest, in state #4 should always be chosen
+// in favor of a network, that satisfies the default NetworkRequest, in state #3.
+// When deciding between two networks, that both satisfy the default NetworkRequest, to select
+// for the default network connection, the one with the higher score should be chosen.
+//
+// When a network disconnects:
+// ---------------------------
+// If a network's transport disappears, for example:
+// a. WiFi turned off, or
+// b. cellular data turned off, or
+// c. airplane mode is turned on, or
+// d. a wireless connection disconnects from AP/cell tower entirely (e.g. device is out of range
+//    of AP for an extended period of time, or switches to another AP without roaming)
+// then that network can transition from any state (#1-#4) to unregistered.  This happens by
+// the transport disconnecting their NetworkAgent's AsyncChannel with ConnectivityManager.
+// ConnectivityService also tells netd to destroy the network.
+//
+// When ConnectivityService disconnects a network:
+// -----------------------------------------------
+// If a network has no chance of satisfying any requests (even if it were to become validated
+// and enter state #4), ConnectivityService will disconnect the NetworkAgent's AsyncChannel.
+// If the network ever for any period of time had satisfied a NetworkRequest (i.e. had been
+// the highest scoring that satisfied the NetworkRequest's constraints), but is no longer the
+// highest scoring network for any NetworkRequest, then there will be a 30s pause before
+// ConnectivityService disconnects the NetworkAgent's AsyncChannel.  During this pause the
+// network is considered "lingering".  This pause exists to allow network communication to be
+// wrapped up rather than abruptly terminated.  During this pause if the network begins satisfying
+// a NetworkRequest, ConnectivityService will cancel the future disconnection of the NetworkAgent's
+// AsyncChannel, and the network is no longer considered "lingering".
 public class NetworkAgentInfo implements Comparable<NetworkAgentInfo> {
     public NetworkInfo networkInfo;
     // This Network object should always be used if possible, so as to encourage reuse of the
@@ -156,7 +214,12 @@ public class NetworkAgentInfo implements Comparable<NetworkAgentInfo> {
         }
 
         int score = currentScore;
-        if (!everValidated && !pretendValidated) score -= UNVALIDATED_SCORE_PENALTY;
+        // Use NET_CAPABILITY_VALIDATED here instead of lastValidated, this allows
+        // ConnectivityService.updateCapabilities() to compute the old score prior to updating
+        // networkCapabilities (with a potentially different validated state).
+        if (!networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) && !pretendValidated) {
+            score -= UNVALIDATED_SCORE_PENALTY;
+        }
         if (score < 0) score = 0;
         return score;
     }
index e472928..5e098d4 100644 (file)
@@ -576,9 +576,12 @@ public class NetworkMonitor extends StateMachine {
             switch (message.what) {
                 case CMD_NETWORK_CONNECTED:
                     log("Unlingered");
-                    // Go straight to active as we've already evaluated.
-                    transitionTo(mValidatedState);
-                    return HANDLED;
+                    // If already validated, go straight to validated state.
+                    if (mNetworkAgentInfo.lastValidated) {
+                        transitionTo(mValidatedState);
+                        return HANDLED;
+                    }
+                    return NOT_HANDLED;
                 case CMD_LINGER_EXPIRED:
                     if (message.arg1 != mLingerToken)
                         return HANDLED;
index c1311ed..cb9c6a7 100644 (file)
@@ -151,6 +151,7 @@ public class ConnectivityServiceTest extends AndroidTestCase {
         private final NetworkInfo mNetworkInfo;
         private final NetworkCapabilities mNetworkCapabilities;
         private final Thread mThread;
+        private final ConditionVariable mDisconnected = new ConditionVariable();
         private int mScore;
         private NetworkAgent mNetworkAgent;
 
@@ -177,7 +178,7 @@ public class ConnectivityServiceTest extends AndroidTestCase {
                     mNetworkAgent = new NetworkAgent(Looper.myLooper(), mServiceContext,
                             "Mock" + typeName, mNetworkInfo, mNetworkCapabilities,
                             new LinkProperties(), mScore, new NetworkMisc()) {
-                        public void unwanted() {}
+                        public void unwanted() { mDisconnected.open(); }
                     };
                     initComplete.open();
                     Looper.loop();
@@ -197,8 +198,13 @@ public class ConnectivityServiceTest extends AndroidTestCase {
             mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
         }
 
+        public void connectWithoutInternet() {
+            mNetworkInfo.setDetailedState(DetailedState.CONNECTED, null, null);
+            mNetworkAgent.sendNetworkInfo(mNetworkInfo);
+        }
+
         /**
-         * Transition this NetworkAgent to CONNECTED state.
+         * Transition this NetworkAgent to CONNECTED state with NET_CAPABILITY_INTERNET.
          * @param validated Indicate if network should pretend to be validated.
          */
         public void connect(boolean validated) {
@@ -231,8 +237,7 @@ public class ConnectivityServiceTest extends AndroidTestCase {
                 mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
             }
 
-            mNetworkInfo.setDetailedState(DetailedState.CONNECTED, null, null);
-            mNetworkAgent.sendNetworkInfo(mNetworkInfo);
+            connectWithoutInternet();
 
             if (validated) {
                 // Wait for network to validate.
@@ -252,6 +257,10 @@ public class ConnectivityServiceTest extends AndroidTestCase {
         public Network getNetwork() {
             return new Network(mNetworkAgent.netId);
         }
+
+        public ConditionVariable getDisconnectedCV() {
+            return mDisconnected;
+        }
     }
 
     private static class MockNetworkFactory extends NetworkFactory {
@@ -576,6 +585,34 @@ public class ConnectivityServiceTest extends AndroidTestCase {
     }
 
     @LargeTest
+    public void testUnlingeringDoesNotValidate() throws Exception {
+        // Test bringing up unvalidated cellular.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mCellNetworkAgent.connect(false);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        // Test bringing up validated WiFi.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        cv = waitForConnectivityBroadcasts(2);
+        mWiFiNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        // Test WiFi disconnect.
+        cv = waitForConnectivityBroadcasts(2);
+        mWiFiNetworkAgent.disconnect();
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Unlingering a network should not cause it to be marked as validated.
+        assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+    }
+
+    @LargeTest
     public void testCellularOutscoresWeakWifi() throws Exception {
         // Test bringing up validated cellular.
         mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
@@ -603,6 +640,107 @@ public class ConnectivityServiceTest extends AndroidTestCase {
         mWiFiNetworkAgent.disconnect();
     }
 
+    @LargeTest
+    public void testReapingNetwork() throws Exception {
+        // Test bringing up WiFi without NET_CAPABILITY_INTERNET.
+        // Expect it to be torn down immediately because it satisfies no requests.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        ConditionVariable cv = mWiFiNetworkAgent.getDisconnectedCV();
+        mWiFiNetworkAgent.connectWithoutInternet();
+        waitFor(cv);
+        // Test bringing up cellular without NET_CAPABILITY_INTERNET.
+        // Expect it to be torn down immediately because it satisfies no requests.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        cv = mCellNetworkAgent.getDisconnectedCV();
+        mCellNetworkAgent.connectWithoutInternet();
+        waitFor(cv);
+        // Test bringing up validated WiFi.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        cv = waitForConnectivityBroadcasts(1);
+        mWiFiNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test bringing up unvalidated cellular.
+        // Expect it to be torn down because it could never be the highest scoring network
+        // satisfying the default request even if it validated.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        cv = mCellNetworkAgent.getDisconnectedCV();
+        mCellNetworkAgent.connect(false);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        cv = mWiFiNetworkAgent.getDisconnectedCV();
+        mWiFiNetworkAgent.disconnect();
+        waitFor(cv);
+    }
+
+    @LargeTest
+    public void testCellularFallback() throws Exception {
+        // Test bringing up validated cellular.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mCellNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test bringing up validated WiFi.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        cv = waitForConnectivityBroadcasts(2);
+        mWiFiNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Reevaluate WiFi (it'll instantly fail DNS).
+        cv = waitForConnectivityBroadcasts(2);
+        assertTrue(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        mCm.reportBadNetwork(mWiFiNetworkAgent.getNetwork());
+        // Should quickly fall back to Cellular.
+        waitFor(cv);
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Reevaluate cellular (it'll instantly fail DNS).
+        cv = waitForConnectivityBroadcasts(2);
+        assertTrue(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        mCm.reportBadNetwork(mCellNetworkAgent.getNetwork());
+        // Should quickly fall back to WiFi.
+        waitFor(cv);
+        assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        mCellNetworkAgent.disconnect();
+        mWiFiNetworkAgent.disconnect();
+    }
+
+    @LargeTest
+    public void testWiFiFallback() throws Exception {
+        // Test bringing up unvalidated WiFi.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mWiFiNetworkAgent.connect(false);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test bringing up validated cellular.
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        cv = waitForConnectivityBroadcasts(2);
+        mCellNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Reevaluate cellular (it'll instantly fail DNS).
+        cv = waitForConnectivityBroadcasts(2);
+        assertTrue(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        mCm.reportBadNetwork(mCellNetworkAgent.getNetwork());
+        // Should quickly fall back to WiFi.
+        waitFor(cv);
+        assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        mCellNetworkAgent.disconnect();
+        mWiFiNetworkAgent.disconnect();
+    }
+
     enum CallbackState {
         NONE,
         AVAILABLE,
@@ -872,6 +1010,71 @@ public class ConnectivityServiceTest extends AndroidTestCase {
         } catch (IllegalArgumentException expected) {}
     }
 
+    @LargeTest
+    public void testMMSonWiFi() throws Exception {
+        // Test bringing up cellular without MMS NetworkRequest gets reaped
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_MMS);
+        ConditionVariable cv = mCellNetworkAgent.getDisconnectedCV();
+        mCellNetworkAgent.connectWithoutInternet();
+        waitFor(cv);
+        waitFor(new Criteria() {
+                public boolean get() { return mCm.getAllNetworks().length == 0; } });
+        verifyNoNetwork();
+        // Test bringing up validated WiFi.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        cv = waitForConnectivityBroadcasts(1);
+        mWiFiNetworkAgent.connect(true);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Register MMS NetworkRequest
+        NetworkRequest.Builder builder = new NetworkRequest.Builder();
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS);
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.requestNetwork(builder.build(), networkCallback);
+        // Test bringing up unvalidated cellular with MMS
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_MMS);
+        cv = networkCallback.getConditionVariable();
+        mCellNetworkAgent.connectWithoutInternet();
+        waitFor(cv);
+        assertEquals(CallbackState.AVAILABLE, networkCallback.getLastCallback());
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test releasing NetworkRequest disconnects cellular with MMS
+        cv = mCellNetworkAgent.getDisconnectedCV();
+        mCm.unregisterNetworkCallback(networkCallback);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+    }
+
+    @LargeTest
+    public void testMMSonCell() throws Exception {
+        // Test bringing up cellular without MMS
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        ConditionVariable cv = waitForConnectivityBroadcasts(1);
+        mCellNetworkAgent.connect(false);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Register MMS NetworkRequest
+        NetworkRequest.Builder builder = new NetworkRequest.Builder();
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS);
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.requestNetwork(builder.build(), networkCallback);
+        // Test bringing up MMS cellular network
+        cv = networkCallback.getConditionVariable();
+        MockNetworkAgent mmsNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mmsNetworkAgent.addCapability(NET_CAPABILITY_MMS);
+        mmsNetworkAgent.connectWithoutInternet();
+        waitFor(cv);
+        assertEquals(CallbackState.AVAILABLE, networkCallback.getLastCallback());
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test releasing MMS NetworkRequest does not disconnect main cellular NetworkAgent
+        cv = mmsNetworkAgent.getDisconnectedCV();
+        mCm.unregisterNetworkCallback(networkCallback);
+        waitFor(cv);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+    }
+
 //    @Override
 //    public void tearDown() throws Exception {
 //        super.tearDown();