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

Commit 52e10954 authored by Chalard Jean's avatar Chalard Jean
Browse files

Make yield-to-bad-wifi behavior backward compatible with R

Yielding cell wins to exiting wifi (whether good or bad).
It loses to bad wifi that's not exiting.

In R, yielding to bad wifi only affects wifis that are
unvalidated, but a wifi that is exiting should still be
dropped in favor of a cell that yields to bad wifi.

I had misunderstood the policy and implemented it wrong.
Now it's implemented right, and has careful tests.

Test: new tests for this
Bug: 186458024
Change-Id: Ib8637100d491e72a2edb837584ce55b7dda58524
parent da438122
Loading
Loading
Loading
Loading
+72 −22
Original line number Diff line number Diff line
@@ -108,7 +108,58 @@ public class NetworkRanker {
        }
    }

    @Nullable private <T extends Scoreable> T getBestNetworkByPolicy(
    private <T extends Scoreable> boolean isBadWiFi(@NonNull final T candidate) {
        return candidate.getScore().hasPolicy(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD)
                && candidate.getCapsNoCopy().hasTransport(TRANSPORT_WIFI);
    }

    /**
     * Apply the "yield to bad WiFi" policy.
     *
     * This function must run immediately after the validation policy.
     *
     * If any of the accepted networks has the "yield to bad WiFi" policy AND there are some
     * bad WiFis in the rejected list, then move the networks with the policy to the rejected
     * list. If this leaves no accepted network, then move the bad WiFis back to the accepted list.
     *
     * This function returns nothing, but will have updated accepted and rejected in-place.
     *
     * @param accepted networks accepted by the validation policy
     * @param rejected networks rejected by the validation policy
     */
    private <T extends Scoreable> void applyYieldToBadWifiPolicy(@NonNull ArrayList<T> accepted,
            @NonNull ArrayList<T> rejected) {
        if (!CollectionUtils.any(accepted, n -> n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI))) {
            // No network with the policy : do nothing.
            return;
        }
        if (!CollectionUtils.any(rejected, n -> isBadWiFi(n))) {
            // No bad WiFi : do nothing.
            return;
        }
        if (CollectionUtils.all(accepted, n -> n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI))) {
            // All validated networks yield to bad WiFis : keep bad WiFis alongside with the
            // yielders. This is important because the yielders need to be compared to the bad
            // wifis by the following policies (e.g. exiting).
            final ArrayList<T> acceptedYielders = new ArrayList<>(accepted);
            final ArrayList<T> rejectedWithBadWiFis = new ArrayList<>(rejected);
            partitionInto(rejectedWithBadWiFis, n -> isBadWiFi(n), accepted, rejected);
            accepted.addAll(acceptedYielders);
            return;
        }
        // Only some of the validated networks yield to bad WiFi : keep only the ones who don't.
        final ArrayList<T> acceptedWithYielders = new ArrayList<>(accepted);
        partitionInto(acceptedWithYielders, n -> !n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI),
                accepted, rejected);
    }

    /**
     * Get the best network among a list of candidates according to policy.
     * @param candidates the candidates
     * @param currentSatisfier the current satisfier, or null if none
     * @return the best network
     */
    @Nullable public <T extends Scoreable> T getBestNetworkByPolicy(
            @NonNull List<T> candidates,
            @Nullable final T currentSatisfier) {
        // Used as working areas.
@@ -148,24 +199,15 @@ public class NetworkRanker {
        if (accepted.size() == 1) return accepted.get(0);
        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);

        // Yield to bad wifi policy : if any wifi has ever been validated (even if it's now
        // unvalidated), and unless it's been explicitly avoided when bad in UI, then keep only
        // networks that don't yield to such a wifi network.
        final boolean anyWiFiEverValidated = CollectionUtils.any(candidates,
                nai -> nai.getScore().hasPolicy(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD)
                        && nai.getCapsNoCopy().hasTransport(TRANSPORT_WIFI));
        if (anyWiFiEverValidated) {
            partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI),
                    accepted, rejected);
            if (accepted.size() == 1) return accepted.get(0);
            if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
        }

        // If any network is validated (or should be accepted even if it's not validated), then
        // don't choose one that isn't.
        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VALIDATED)
                        || nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED),
                accepted, rejected);
        // Yield to bad wifi policy : if any network has the "yield to bad WiFi" policy and
        // there are bad WiFis connected, then accept the bad WiFis and reject the networks with
        // the policy.
        applyYieldToBadWifiPolicy(accepted, rejected);
        if (accepted.size() == 1) return accepted.get(0);
        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);

@@ -194,16 +236,24 @@ public class NetworkRanker {
        // subscription with the same transport.
        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY),
                accepted, rejected);
        for (final Scoreable defaultSubNai : accepted) {
            // Remove all networks without the DEFAULT_SUBSCRIPTION policy and the same transports
            // as a network that has it.
        if (accepted.size() > 0) {
            // Some networks are primary. For each transport, keep only the primary, but also
            // keep all networks for which there isn't a primary (which are now in the |rejected|
            // array).
            // So for each primary network, remove from |rejected| all networks with the same
            // transports as one of the primary networks. The remaining networks should be accepted.
            for (final T defaultSubNai : accepted) {
                final int[] transports = defaultSubNai.getCapsNoCopy().getTransportTypes();
            candidates.removeIf(nai -> !nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY)
                    && Arrays.equals(transports, nai.getCapsNoCopy().getTransportTypes()));
                rejected.removeIf(
                        nai -> Arrays.equals(transports, nai.getCapsNoCopy().getTransportTypes()));
            }
            accepted.addAll(rejected);
            candidates = new ArrayList<>(accepted);
        }
        if (1 == candidates.size()) return candidates.get(0);
        // It's guaranteed candidates.size() > 0 because there is at least one with the
        // TRANSPORT_PRIMARY policy and only those without it were removed.
        // If there were no primary network, then candidates.size() > 0 because it didn't
        // change from the previous result. If there were, it's guaranteed candidates.size() > 0
        // because accepted.size() > 0 above.

        // If some of the networks have a better transport than others, keep only the ones with
        // the best transports.
+128 −46
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 * Copyright (C) 2021 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.
@@ -17,74 +17,156 @@
package com.android.server.connectivity

import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkScore.KEEP_CONNECTED_NONE
import android.net.NetworkScore.POLICY_EXITING
import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY
import android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI
import android.os.Build
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
import com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD
import com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import kotlin.test.assertEquals
import kotlin.test.assertNull

@RunWith(AndroidJUnit4::class)
private fun score(vararg policies: Int) = FullScore(0,
        policies.fold(0L) { acc, e -> acc or (1L shl e) }, KEEP_CONNECTED_NONE)
private fun caps(transport: Int) = NetworkCapabilities.Builder().addTransportType(transport).build()

@SmallTest
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
class NetworkRankerTest {
    private val ranker = NetworkRanker()
    private val mRanker = NetworkRanker()

    private class TestScore(private val sc: FullScore, private val nc: NetworkCapabilities)
            : NetworkRanker.Scoreable {
        override fun getScore() = sc
        override fun getCapsNoCopy(): NetworkCapabilities = nc
    }

    @Test
    fun testYieldToBadWiFiOneCell() {
        // Only cell, it wins
        val winner = TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                caps(TRANSPORT_CELLULAR))
        val scores = listOf(winner)
        assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
    }

    private fun makeNai(satisfy: Boolean, legacyScore: Int) =
            mock(NetworkAgentInfo::class.java).also {
                doReturn(satisfy).`when`(it).satisfies(any())
                val fs = FullScore(legacyScore, 0 /* policies */, KEEP_CONNECTED_NONE)
                doReturn(fs).`when`(it).getScore()
                val nc = NetworkCapabilities.Builder().build()
                doReturn(nc).`when`(it).getCapsNoCopy()
    @Test
    fun testYieldToBadWiFiOneCellOneBadWiFi() {
        // Bad wifi wins against yielding validated cell
        val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD),
                caps(TRANSPORT_WIFI))
        val scores = listOf(
                winner,
                TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                        caps(TRANSPORT_CELLULAR))
        )
        assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
    }

    @Test
    fun testYieldToBadWiFiOneCellTwoBadWiFi() {
        // Bad wifi wins against yielding validated cell. Prefer the one that's primary.
        val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
                POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI))
        val scores = listOf(
                winner,
                TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD),
                        caps(TRANSPORT_WIFI)),
                TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                        caps(TRANSPORT_CELLULAR))
        )
        assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
    }

    @Test
    fun testGetBestNetwork() {
        val scores = listOf(20, 50, 90, 60, 23, 68)
        val nais = scores.map { makeNai(true, it) }
        val bestNetwork = nais[2] // The one with the top score
        val someRequest = mock(NetworkRequest::class.java)
        assertEquals(bestNetwork, ranker.getBestNetwork(someRequest, nais, bestNetwork))
    fun testYieldToBadWiFiOneCellTwoBadWiFiOneNotAvoided() {
        // Bad wifi ever validated wins against bad wifi that never was validated (or was
        // avoided when bad).
        val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD),
                caps(TRANSPORT_WIFI))
        val scores = listOf(
                winner,
                TestScore(score(), caps(TRANSPORT_WIFI)),
                TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                        caps(TRANSPORT_CELLULAR))
        )
        assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
    }

    @Test
    fun testIgnoreNonSatisfying() {
        val nais = listOf(makeNai(true, 20), makeNai(true, 50), makeNai(false, 90),
                makeNai(false, 60), makeNai(true, 23), makeNai(false, 68))
        val bestNetwork = nais[1] // Top score that's satisfying
        val someRequest = mock(NetworkRequest::class.java)
        assertEquals(bestNetwork, ranker.getBestNetwork(someRequest, nais, nais[1]))
    fun testYieldToBadWiFiOneCellOneBadWiFiOneGoodWiFi() {
        // Good wifi wins
        val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
                POLICY_IS_VALIDATED), caps(TRANSPORT_WIFI))
        val scores = listOf(
                winner,
                TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
                        POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI)),
                TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                        caps(TRANSPORT_CELLULAR))
        )
        assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
    }

    @Test
    fun testNoMatch() {
        val nais = listOf(makeNai(false, 20), makeNai(false, 50), makeNai(false, 90))
        val someRequest = mock(NetworkRequest::class.java)
        assertNull(ranker.getBestNetwork(someRequest, nais, null))
    fun testYieldToBadWiFiTwoCellsOneBadWiFi() {
        // Cell that doesn't yield wins over cell that yields and bad wifi
        val winner = TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_CELLULAR))
        val scores = listOf(
                winner,
                TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
                        POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI)),
                TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                        caps(TRANSPORT_CELLULAR))
        )
        assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
    }

    @Test
    fun testEmpty() {
        val someRequest = mock(NetworkRequest::class.java)
        assertNull(ranker.getBestNetwork(someRequest, emptyList(), null))
    fun testYieldToBadWiFiTwoCellsOneBadWiFiOneGoodWiFi() {
        // Good wifi wins over cell that doesn't yield and cell that yields
        val winner = TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_WIFI))
        val scores = listOf(
                winner,
                TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
                        POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI)),
                TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_CELLULAR)),
                TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                        caps(TRANSPORT_CELLULAR))
        )
        assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
    }

    // Make sure the ranker is "stable" (as in stable sort), that is, it always returns the FIRST
    // network satisfying the request if multiple of them have the same score.
    @Test
    fun testStable() {
        val nais1 = listOf(makeNai(true, 30), makeNai(true, 30), makeNai(true, 30),
                makeNai(true, 30), makeNai(true, 30), makeNai(true, 30))
        val someRequest = mock(NetworkRequest::class.java)
        assertEquals(nais1[0], ranker.getBestNetwork(someRequest, nais1, nais1[0]))
    fun testYieldToBadWiFiOneExitingGoodWiFi() {
        // Yielding cell wins over good exiting wifi
        val winner = TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                caps(TRANSPORT_CELLULAR))
        val scores = listOf(
                winner,
                TestScore(score(POLICY_IS_VALIDATED, POLICY_EXITING), caps(TRANSPORT_WIFI))
        )
        assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
    }

        val nais2 = listOf(makeNai(true, 30), makeNai(true, 50), makeNai(true, 20),
                makeNai(true, 50), makeNai(true, 50), makeNai(true, 40))
        assertEquals(nais2[1], ranker.getBestNetwork(someRequest, nais2, nais2[1]))
    @Test
    fun testYieldToBadWiFiOneExitingBadWiFi() {
        // Yielding cell wins over bad exiting wifi
        val winner = TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                caps(TRANSPORT_CELLULAR))
        val scores = listOf(
                winner,
                TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD,
                        POLICY_EXITING), caps(TRANSPORT_WIFI))
        )
        assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
    }
}