Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bbce221e authored by Paul Jensen's avatar Paul Jensen Committed by Android (Google) Code Review
Browse files

Merge "Fallback to Cellular if WiFi fails to validate" into mnc-dev

parents d8cdba0d e098854c
Loading
Loading
Loading
Loading
+40 −49
Original line number Diff line number Diff line
@@ -2064,12 +2064,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);
    }

@@ -2224,23 +2222,10 @@ 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.
@@ -2255,12 +2240,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
                    //    WiFi ends up validating and out scoring cellular.
                    mNetworkForRequestId.get(nri.request.requestId).getCurrentScore() <
                            nai.getCurrentScoreAsValidated())) {
                    unneeded = false;
                    break;
                }
                return false;
            }
        }
        return unneeded;
        return true;
    }

    private void handleReleaseNetworkRequest(NetworkRequest request, int callingUid) {
@@ -4004,29 +3987,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);
        }
    }

@@ -4256,9 +4239,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);
            }
        }
@@ -4292,7 +4283,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);
            }
+64 −1
Original line number Diff line number Diff line
@@ -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;
    }
+6 −3
Original line number Diff line number Diff line
@@ -585,9 +585,12 @@ public class NetworkMonitor extends StateMachine {
            switch (message.what) {
                case CMD_NETWORK_CONNECTED:
                    log("Unlingered");
                    // Go straight to active as we've already evaluated.
                    // 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;
+207 −4
Original line number Diff line number Diff line
@@ -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 {
@@ -575,6 +584,34 @@ public class ConnectivityServiceTest extends AndroidTestCase {
        verifyNoNetwork();
    }

    @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.
@@ -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();