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