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