summaryrefslogtreecommitdiff
path: root/service/src/com/android/server/connectivity/NetworkRanker.java
blob: babc3531bd8c7d1516dbc6472decaa8c00ff51f8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.connectivity;

import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkScore.POLICY_EXITING;
import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY;
import static android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI;

import static com.android.net.module.util.CollectionUtils.filter;
import static com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED;
import static com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED;
import static com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD;
import static com.android.server.connectivity.FullScore.POLICY_IS_DESTROYED;
import static com.android.server.connectivity.FullScore.POLICY_IS_INVINCIBLE;
import static com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED;
import static com.android.server.connectivity.FullScore.POLICY_IS_VPN;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;

import com.android.net.module.util.CollectionUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;

/**
 * A class that knows how to find the best network matching a request out of a list of networks.
 */
public class NetworkRanker {
    // Historically the legacy ints have been 0~100 in principle (though the highest score in
    // AOSP has always been 90). This is relied on by VPNs that send a legacy score of 101.
    public static final int LEGACY_INT_MAX = 100;

    /**
     * A class that can be scored against other scoreables.
     */
    public interface Scoreable {
        /** Get score of this scoreable */
        FullScore getScore();
        /** Get capabilities of this scoreable */
        NetworkCapabilities getCapsNoCopy();
    }

    public NetworkRanker() { }

    /**
     * Find the best network satisfying this request among the list of passed networks.
     */
    @Nullable
    public NetworkAgentInfo getBestNetwork(@NonNull final NetworkRequest request,
            @NonNull final Collection<NetworkAgentInfo> nais,
            @Nullable final NetworkAgentInfo currentSatisfier) {
        final ArrayList<NetworkAgentInfo> candidates = filter(nais, nai -> nai.satisfies(request));
        if (candidates.size() == 1) return candidates.get(0); // Only one potential satisfier
        if (candidates.size() <= 0) return null; // No network can satisfy this request
        return getBestNetworkByPolicy(candidates, currentSatisfier);
    }

    // Transport preference order, if it comes down to that.
    private static final int[] PREFERRED_TRANSPORTS_ORDER = { TRANSPORT_ETHERNET, TRANSPORT_WIFI,
            TRANSPORT_BLUETOOTH, TRANSPORT_CELLULAR };

    // Function used to partition a list into two working areas depending on whether they
    // satisfy a predicate. All items satisfying the predicate will be put in |positive|, all
    // items that don't will be put in |negative|.
    // This is useful in this file because many of the ranking checks will retain only networks that
    // satisfy a predicate if any of them do, but keep them all if all of them do. Having working
    // areas is uncustomary in Java, but this function is called in a fairly intensive manner
    // and doing allocation quite that often might affect performance quite badly.
    private static <T> void partitionInto(@NonNull final List<T> source, @NonNull Predicate<T> test,
            @NonNull final List<T> positive, @NonNull final List<T> negative) {
        positive.clear();
        negative.clear();
        for (final T item : source) {
            if (test.test(item)) {
                positive.add(item);
            } else {
                negative.add(item);
            }
        }
    }

    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.
        final ArrayList<T> accepted =
                new ArrayList<>(candidates.size() /* initialCapacity */);
        final ArrayList<T> rejected =
                new ArrayList<>(candidates.size() /* initialCapacity */);

        // The following tests will search for a network matching a given criterion. They all
        // function the same way : if any network matches the criterion, drop from consideration
        // all networks that don't. To achieve this, the tests below :
        // 1. partition the list of remaining candidates into accepted and rejected networks.
        // 2. if only one candidate remains, that's the winner : if accepted.size == 1 return [0]
        // 3. if multiple remain, keep only the accepted networks and go on to the next criterion.
        //    Because the working areas will be wiped, a copy of the accepted networks needs to be
        //    made.
        // 4. if none remain, the criterion did not help discriminate so keep them all. As an
        //    optimization, skip creating a new array and go on to the next criterion.

        // If a network is invincible, use it.
        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_INVINCIBLE),
                accepted, rejected);
        if (accepted.size() == 1) return accepted.get(0);
        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);

        // If there is a connected VPN, use it.
        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VPN),
                accepted, rejected);
        if (accepted.size() == 1) return accepted.get(0);
        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);

        // Selected & Accept-unvalidated policy : if any network has both of these, then don't
        // choose one that doesn't.
        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_EVER_USER_SELECTED)
                        && nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED),
                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);

        // If any network is not exiting, don't choose one that is.
        partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_EXITING),
                accepted, rejected);
        if (accepted.size() == 1) return accepted.get(0);
        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);

        // TODO : If any network is unmetered, don't choose a metered network.
        // This can't be implemented immediately because prospective networks are always
        // considered unmetered because factories don't know if the network will be metered.
        // Saying an unmetered network always beats a metered one would mean that when metered wifi
        // is connected, the offer for telephony would beat WiFi but the actual metered network
        // would lose, so we'd have an infinite loop where telephony would continually bring up
        // a network that is immediately torn down.
        // Fix this by getting the agent to tell connectivity whether the network they will
        // bring up is metered. Cell knows that in advance, while WiFi has a good estimate and
        // can revise it if the network later turns out to be metered.
        // partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_UNMETERED),
        //         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 for the default subscription, don't choose a network for another
        // subscription with the same transport.
        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY),
                accepted, rejected);
        if (accepted.size() > 0) {
            // Some networks are primary for their transport. 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();
                rejected.removeIf(
                        nai -> Arrays.equals(transports, nai.getCapsNoCopy().getTransportTypes()));
            }
            // Now the |rejected| list contains networks with transports for which there isn't
            // a primary network. Add them back to the candidates.
            accepted.addAll(rejected);
            candidates = new ArrayList<>(accepted);
        }
        if (1 == candidates.size()) return candidates.get(0);
        // 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.
        for (final int transport : PREFERRED_TRANSPORTS_ORDER) {
            partitionInto(candidates, nai -> nai.getCapsNoCopy().hasTransport(transport),
                    accepted, rejected);
            if (accepted.size() == 1) return accepted.get(0);
            if (accepted.size() > 0 && rejected.size() > 0) {
                candidates = new ArrayList<>(accepted);
                break;
            }
        }

        // If two networks are equivalent, and one has been destroyed pending replacement, keep the
        // other one. This ensures that when the replacement connects, it's preferred.
        partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_IS_DESTROYED),
                accepted, rejected);
        if (accepted.size() == 1) return accepted.get(0);
        if (accepted.size() > 0 && rejected.size() > 0) {
            candidates = new ArrayList<>(accepted);
        }

        // At this point there are still multiple networks passing all the tests above. If any
        // of them is the previous satisfier, keep it.
        if (candidates.contains(currentSatisfier)) return currentSatisfier;

        // If there are still multiple options at this point but none of them is any of the
        // transports above, it doesn't matter which is returned. They are all the same.
        return candidates.get(0);
    }

    /**
     * Returns whether a {@link Scoreable} has a chance to beat a champion network for a request.
     *
     * Offers are sent by network providers when they think they might be able to make a network
     * with the characteristics contained in the offer. If the offer has no chance to beat
     * the currently best network for a given request, there is no point in the provider spending
     * power trying to find and bring up such a network.
     *
     * Note that having an offer up does not constitute a commitment from the provider part
     * to be able to bring up a network with these characteristics, or a network at all for
     * that matter. This is only used to save power by letting providers know when they can't
     * beat a current champion.
     *
     * @param request The request to evaluate against.
     * @param champion The currently best network for this request.
     * @param contestant The offer.
     * @return Whether the offer stands a chance to beat the champion.
     */
    public boolean mightBeat(@NonNull final NetworkRequest request,
            @Nullable final NetworkAgentInfo champion,
            @NonNull final Scoreable contestant) {
        // If this network can't even satisfy the request then it can't beat anything, not
        // even an absence of network. It can't satisfy it anyway.
        if (!request.canBeSatisfiedBy(contestant.getCapsNoCopy())) return false;
        // If there is no satisfying network, then this network can beat, because some network
        // is always better than no network.
        if (null == champion) return true;
        // If there is no champion, the offer can always beat.
        // Otherwise rank them.
        final ArrayList<Scoreable> candidates = new ArrayList<>();
        candidates.add(champion);
        candidates.add(contestant);
        return contestant == getBestNetworkByPolicy(candidates, champion);
    }
}