• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.connectivity;
18 
19 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
20 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
21 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
22 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
23 import static android.net.NetworkScore.POLICY_EXITING;
24 import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY;
25 import static android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI;
26 
27 import static com.android.net.module.util.CollectionUtils.filter;
28 import static com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED;
29 import static com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED;
30 import static com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD;
31 import static com.android.server.connectivity.FullScore.POLICY_IS_INVINCIBLE;
32 import static com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED;
33 import static com.android.server.connectivity.FullScore.POLICY_IS_VPN;
34 
35 import android.annotation.NonNull;
36 import android.annotation.Nullable;
37 import android.net.NetworkCapabilities;
38 import android.net.NetworkRequest;
39 
40 import com.android.net.module.util.CollectionUtils;
41 
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.Collection;
45 import java.util.List;
46 import java.util.function.Predicate;
47 
48 /**
49  * A class that knows how to find the best network matching a request out of a list of networks.
50  */
51 public class NetworkRanker {
52     // Historically the legacy ints have been 0~100 in principle (though the highest score in
53     // AOSP has always been 90). This is relied on by VPNs that send a legacy score of 101.
54     public static final int LEGACY_INT_MAX = 100;
55 
56     /**
57      * A class that can be scored against other scoreables.
58      */
59     public interface Scoreable {
60         /** Get score of this scoreable */
getScore()61         FullScore getScore();
62         /** Get capabilities of this scoreable */
getCapsNoCopy()63         NetworkCapabilities getCapsNoCopy();
64     }
65 
66     private static final boolean USE_POLICY_RANKING = true;
67 
NetworkRanker()68     public NetworkRanker() { }
69 
70     /**
71      * Find the best network satisfying this request among the list of passed networks.
72      */
73     @Nullable
getBestNetwork(@onNull final NetworkRequest request, @NonNull final Collection<NetworkAgentInfo> nais, @Nullable final NetworkAgentInfo currentSatisfier)74     public NetworkAgentInfo getBestNetwork(@NonNull final NetworkRequest request,
75             @NonNull final Collection<NetworkAgentInfo> nais,
76             @Nullable final NetworkAgentInfo currentSatisfier) {
77         final ArrayList<NetworkAgentInfo> candidates = filter(nais, nai -> nai.satisfies(request));
78         if (candidates.size() == 1) return candidates.get(0); // Only one potential satisfier
79         if (candidates.size() <= 0) return null; // No network can satisfy this request
80         if (USE_POLICY_RANKING) {
81             return getBestNetworkByPolicy(candidates, currentSatisfier);
82         } else {
83             return getBestNetworkByLegacyInt(candidates);
84         }
85     }
86 
87     // Transport preference order, if it comes down to that.
88     private static final int[] PREFERRED_TRANSPORTS_ORDER = { TRANSPORT_ETHERNET, TRANSPORT_WIFI,
89             TRANSPORT_BLUETOOTH, TRANSPORT_CELLULAR };
90 
91     // Function used to partition a list into two working areas depending on whether they
92     // satisfy a predicate. All items satisfying the predicate will be put in |positive|, all
93     // items that don't will be put in |negative|.
94     // This is useful in this file because many of the ranking checks will retain only networks that
95     // satisfy a predicate if any of them do, but keep them all if all of them do. Having working
96     // areas is uncustomary in Java, but this function is called in a fairly intensive manner
97     // and doing allocation quite that often might affect performance quite badly.
partitionInto(@onNull final List<T> source, @NonNull Predicate<T> test, @NonNull final List<T> positive, @NonNull final List<T> negative)98     private static <T> void partitionInto(@NonNull final List<T> source, @NonNull Predicate<T> test,
99             @NonNull final List<T> positive, @NonNull final List<T> negative) {
100         positive.clear();
101         negative.clear();
102         for (final T item : source) {
103             if (test.test(item)) {
104                 positive.add(item);
105             } else {
106                 negative.add(item);
107             }
108         }
109     }
110 
isBadWiFi(@onNull final T candidate)111     private <T extends Scoreable> boolean isBadWiFi(@NonNull final T candidate) {
112         return candidate.getScore().hasPolicy(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD)
113                 && candidate.getCapsNoCopy().hasTransport(TRANSPORT_WIFI);
114     }
115 
116     /**
117      * Apply the "yield to bad WiFi" policy.
118      *
119      * This function must run immediately after the validation policy.
120      *
121      * If any of the accepted networks has the "yield to bad WiFi" policy AND there are some
122      * bad WiFis in the rejected list, then move the networks with the policy to the rejected
123      * list. If this leaves no accepted network, then move the bad WiFis back to the accepted list.
124      *
125      * This function returns nothing, but will have updated accepted and rejected in-place.
126      *
127      * @param accepted networks accepted by the validation policy
128      * @param rejected networks rejected by the validation policy
129      */
applyYieldToBadWifiPolicy(@onNull ArrayList<T> accepted, @NonNull ArrayList<T> rejected)130     private <T extends Scoreable> void applyYieldToBadWifiPolicy(@NonNull ArrayList<T> accepted,
131             @NonNull ArrayList<T> rejected) {
132         if (!CollectionUtils.any(accepted, n -> n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI))) {
133             // No network with the policy : do nothing.
134             return;
135         }
136         if (!CollectionUtils.any(rejected, n -> isBadWiFi(n))) {
137             // No bad WiFi : do nothing.
138             return;
139         }
140         if (CollectionUtils.all(accepted, n -> n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI))) {
141             // All validated networks yield to bad WiFis : keep bad WiFis alongside with the
142             // yielders. This is important because the yielders need to be compared to the bad
143             // wifis by the following policies (e.g. exiting).
144             final ArrayList<T> acceptedYielders = new ArrayList<>(accepted);
145             final ArrayList<T> rejectedWithBadWiFis = new ArrayList<>(rejected);
146             partitionInto(rejectedWithBadWiFis, n -> isBadWiFi(n), accepted, rejected);
147             accepted.addAll(acceptedYielders);
148             return;
149         }
150         // Only some of the validated networks yield to bad WiFi : keep only the ones who don't.
151         final ArrayList<T> acceptedWithYielders = new ArrayList<>(accepted);
152         partitionInto(acceptedWithYielders, n -> !n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI),
153                 accepted, rejected);
154     }
155 
156     /**
157      * Get the best network among a list of candidates according to policy.
158      * @param candidates the candidates
159      * @param currentSatisfier the current satisfier, or null if none
160      * @return the best network
161      */
getBestNetworkByPolicy( @onNull List<T> candidates, @Nullable final T currentSatisfier)162     @Nullable public <T extends Scoreable> T getBestNetworkByPolicy(
163             @NonNull List<T> candidates,
164             @Nullable final T currentSatisfier) {
165         // Used as working areas.
166         final ArrayList<T> accepted =
167                 new ArrayList<>(candidates.size() /* initialCapacity */);
168         final ArrayList<T> rejected =
169                 new ArrayList<>(candidates.size() /* initialCapacity */);
170 
171         // The following tests will search for a network matching a given criterion. They all
172         // function the same way : if any network matches the criterion, drop from consideration
173         // all networks that don't. To achieve this, the tests below :
174         // 1. partition the list of remaining candidates into accepted and rejected networks.
175         // 2. if only one candidate remains, that's the winner : if accepted.size == 1 return [0]
176         // 3. if multiple remain, keep only the accepted networks and go on to the next criterion.
177         //    Because the working areas will be wiped, a copy of the accepted networks needs to be
178         //    made.
179         // 4. if none remain, the criterion did not help discriminate so keep them all. As an
180         //    optimization, skip creating a new array and go on to the next criterion.
181 
182         // If a network is invincible, use it.
183         partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_INVINCIBLE),
184                 accepted, rejected);
185         if (accepted.size() == 1) return accepted.get(0);
186         if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
187 
188         // If there is a connected VPN, use it.
189         partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VPN),
190                 accepted, rejected);
191         if (accepted.size() == 1) return accepted.get(0);
192         if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
193 
194         // Selected & Accept-unvalidated policy : if any network has both of these, then don't
195         // choose one that doesn't.
196         partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_EVER_USER_SELECTED)
197                         && nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED),
198                 accepted, rejected);
199         if (accepted.size() == 1) return accepted.get(0);
200         if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
201 
202         // If any network is validated (or should be accepted even if it's not validated), then
203         // don't choose one that isn't.
204         partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VALIDATED)
205                         || nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED),
206                 accepted, rejected);
207         // Yield to bad wifi policy : if any network has the "yield to bad WiFi" policy and
208         // there are bad WiFis connected, then accept the bad WiFis and reject the networks with
209         // the policy.
210         applyYieldToBadWifiPolicy(accepted, rejected);
211         if (accepted.size() == 1) return accepted.get(0);
212         if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
213 
214         // If any network is not exiting, don't choose one that is.
215         partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_EXITING),
216                 accepted, rejected);
217         if (accepted.size() == 1) return accepted.get(0);
218         if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
219 
220         // TODO : If any network is unmetered, don't choose a metered network.
221         // This can't be implemented immediately because prospective networks are always
222         // considered unmetered because factories don't know if the network will be metered.
223         // Saying an unmetered network always beats a metered one would mean that when metered wifi
224         // is connected, the offer for telephony would beat WiFi but the actual metered network
225         // would lose, so we'd have an infinite loop where telephony would continually bring up
226         // a network that is immediately torn down.
227         // Fix this by getting the agent to tell connectivity whether the network they will
228         // bring up is metered. Cell knows that in advance, while WiFi has a good estimate and
229         // can revise it if the network later turns out to be metered.
230         // partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_UNMETERED),
231         //         accepted, rejected);
232         // if (accepted.size() == 1) return accepted.get(0);
233         // if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
234 
235         // If any network is for the default subscription, don't choose a network for another
236         // subscription with the same transport.
237         partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY),
238                 accepted, rejected);
239         if (accepted.size() > 0) {
240             // Some networks are primary for their transport. For each transport, keep only the
241             // primary, but also keep all networks for which there isn't a primary (which are now
242             // in the |rejected| array).
243             // So for each primary network, remove from |rejected| all networks with the same
244             // transports as one of the primary networks. The remaining networks should be accepted.
245             for (final T defaultSubNai : accepted) {
246                 final int[] transports = defaultSubNai.getCapsNoCopy().getTransportTypes();
247                 rejected.removeIf(
248                         nai -> Arrays.equals(transports, nai.getCapsNoCopy().getTransportTypes()));
249             }
250             // Now the |rejected| list contains networks with transports for which there isn't
251             // a primary network. Add them back to the candidates.
252             accepted.addAll(rejected);
253             candidates = new ArrayList<>(accepted);
254         }
255         if (1 == candidates.size()) return candidates.get(0);
256         // If there were no primary network, then candidates.size() > 0 because it didn't
257         // change from the previous result. If there were, it's guaranteed candidates.size() > 0
258         // because accepted.size() > 0 above.
259 
260         // If some of the networks have a better transport than others, keep only the ones with
261         // the best transports.
262         for (final int transport : PREFERRED_TRANSPORTS_ORDER) {
263             partitionInto(candidates, nai -> nai.getCapsNoCopy().hasTransport(transport),
264                     accepted, rejected);
265             if (accepted.size() == 1) return accepted.get(0);
266             if (accepted.size() > 0 && rejected.size() > 0) {
267                 candidates = new ArrayList<>(accepted);
268                 break;
269             }
270         }
271 
272         // At this point there are still multiple networks passing all the tests above. If any
273         // of them is the previous satisfier, keep it.
274         if (candidates.contains(currentSatisfier)) return currentSatisfier;
275 
276         // If there are still multiple options at this point but none of them is any of the
277         // transports above, it doesn't matter which is returned. They are all the same.
278         return candidates.get(0);
279     }
280 
281     // TODO : switch to the policy implementation and remove
282     // Almost equivalent to Collections.max(nais), but allows returning null if no network
283     // satisfies the request.
getBestNetworkByLegacyInt( @onNull final Collection<NetworkAgentInfo> nais)284     private NetworkAgentInfo getBestNetworkByLegacyInt(
285             @NonNull final Collection<NetworkAgentInfo> nais) {
286         NetworkAgentInfo bestNetwork = null;
287         int bestScore = Integer.MIN_VALUE;
288         for (final NetworkAgentInfo nai : nais) {
289             final int naiScore = nai.getCurrentScore();
290             if (naiScore > bestScore) {
291                 bestNetwork = nai;
292                 bestScore = naiScore;
293             }
294         }
295         return bestNetwork;
296     }
297 
298     /**
299      * Returns whether a {@link Scoreable} has a chance to beat a champion network for a request.
300      *
301      * Offers are sent by network providers when they think they might be able to make a network
302      * with the characteristics contained in the offer. If the offer has no chance to beat
303      * the currently best network for a given request, there is no point in the provider spending
304      * power trying to find and bring up such a network.
305      *
306      * Note that having an offer up does not constitute a commitment from the provider part
307      * to be able to bring up a network with these characteristics, or a network at all for
308      * that matter. This is only used to save power by letting providers know when they can't
309      * beat a current champion.
310      *
311      * @param request The request to evaluate against.
312      * @param champion The currently best network for this request.
313      * @param contestant The offer.
314      * @return Whether the offer stands a chance to beat the champion.
315      */
mightBeat(@onNull final NetworkRequest request, @Nullable final NetworkAgentInfo champion, @NonNull final Scoreable contestant)316     public boolean mightBeat(@NonNull final NetworkRequest request,
317             @Nullable final NetworkAgentInfo champion,
318             @NonNull final Scoreable contestant) {
319         // If this network can't even satisfy the request then it can't beat anything, not
320         // even an absence of network. It can't satisfy it anyway.
321         if (!request.canBeSatisfiedBy(contestant.getCapsNoCopy())) return false;
322         // If there is no satisfying network, then this network can beat, because some network
323         // is always better than no network.
324         if (null == champion) return true;
325         if (USE_POLICY_RANKING) {
326             // If there is no champion, the offer can always beat.
327             // Otherwise rank them.
328             final ArrayList<Scoreable> candidates = new ArrayList<>();
329             candidates.add(champion);
330             candidates.add(contestant);
331             return contestant == getBestNetworkByPolicy(candidates, champion);
332         } else {
333             return mightBeatByLegacyInt(champion.getScore(), contestant);
334         }
335     }
336 
337     /**
338      * Returns whether a contestant might beat a champion according to the legacy int.
339      */
mightBeatByLegacyInt(@ullable final FullScore championScore, @NonNull final Scoreable contestant)340     private boolean mightBeatByLegacyInt(@Nullable final FullScore championScore,
341             @NonNull final Scoreable contestant) {
342         final int offerIntScore;
343         if (contestant.getCapsNoCopy().hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
344             // If the offer might have Internet access, then it might validate.
345             offerIntScore = contestant.getScore().getLegacyIntAsValidated();
346         } else {
347             offerIntScore = contestant.getScore().getLegacyInt();
348         }
349         return championScore.getLegacyInt() < offerIntScore;
350     }
351 }
352