1/*
2 * Copyright (C) 2016 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
17package com.android.server.wifi;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.Context;
22import android.net.NetworkKey;
23import android.net.wifi.ScanResult;
24import android.net.wifi.WifiConfiguration;
25import android.net.wifi.WifiInfo;
26import android.text.TextUtils;
27import android.util.LocalLog;
28import android.util.Pair;
29
30import com.android.internal.R;
31import com.android.internal.annotations.VisibleForTesting;
32import com.android.server.wifi.util.ScanResultUtil;
33
34import java.util.ArrayList;
35import java.util.HashSet;
36import java.util.List;
37
38/**
39 * This class looks at all the connectivity scan results then
40 * selects a network for the phone to connect or roam to.
41 */
42public class WifiNetworkSelector {
43    private static final String TAG = "WifiNetworkSelector";
44
45    private static final long INVALID_TIME_STAMP = Long.MIN_VALUE;
46    // Minimum time gap between last successful network selection and a new selection
47    // attempt.
48    @VisibleForTesting
49    public static final int MINIMUM_NETWORK_SELECTION_INTERVAL_MS = 10 * 1000;
50
51    private final WifiConfigManager mWifiConfigManager;
52    private final Clock mClock;
53    private final LocalLog mLocalLog;
54    private long mLastNetworkSelectionTimeStamp = INVALID_TIME_STAMP;
55    // Buffer of filtered scan results (Scan results considered by network selection) & associated
56    // WifiConfiguration (if any).
57    private volatile List<Pair<ScanDetail, WifiConfiguration>> mConnectableNetworks =
58            new ArrayList<>();
59    private List<ScanDetail> mFilteredNetworks = new ArrayList<>();
60    private final ScoringParams mScoringParams;
61    private final int mStayOnNetworkMinimumTxRate;
62    private final int mStayOnNetworkMinimumRxRate;
63    private final boolean mEnableAutoJoinWhenAssociated;
64
65    /**
66     * WiFi Network Selector supports various types of networks. Each type can
67     * have its evaluator to choose the best WiFi network for the device to connect
68     * to. When registering a WiFi network evaluator with the WiFi Network Selector,
69     * the priority of the network must be specified, and it must be a value between
70     * 0 and (EVALUATOR_MIN_PIRORITY - 1) with 0 being the highest priority. Wifi
71     * Network Selector iterates through the registered scorers from the highest priority
72     * to the lowest till a network is selected.
73     */
74    public static final int EVALUATOR_MIN_PRIORITY = 6;
75
76    /**
77     * Maximum number of evaluators can be registered with Wifi Network Selector.
78     */
79    public static final int MAX_NUM_EVALUATORS = EVALUATOR_MIN_PRIORITY;
80
81    /**
82     * Interface for WiFi Network Evaluator
83     *
84     * A network scorer evaulates all the networks from the scan results and
85     * recommends the best network in its category to connect or roam to.
86     */
87    public interface NetworkEvaluator {
88        /**
89         * Get the evaluator name.
90         */
91        String getName();
92
93        /**
94         * Update the evaluator.
95         *
96         * Certain evaluators have to be updated with the new scan results. For example
97         * the ExternalScoreEvalutor needs to refresh its Score Cache.
98         *
99         * @param scanDetails    a list of scan details constructed from the scan results
100         */
101        void update(List<ScanDetail> scanDetails);
102
103        /**
104         * Evaluate all the networks from the scan results.
105         *
106         * @param scanDetails    a list of scan details constructed from the scan results
107         * @param currentNetwork configuration of the current connected network
108         *                       or null if disconnected
109         * @param currentBssid   BSSID of the current connected network or null if
110         *                       disconnected
111         * @param connected      a flag to indicate if WifiStateMachine is in connected
112         *                       state
113         * @param untrustedNetworkAllowed a flag to indidate if untrusted networks like
114         *                                ephemeral networks are allowed
115         * @param connectableNetworks     a list of the ScanDetail and WifiConfiguration
116         *                                pair which is used by the WifiLastResortWatchdog
117         * @return configuration of the chosen network;
118         *         null if no network in this category is available.
119         */
120        @Nullable
121        WifiConfiguration evaluateNetworks(List<ScanDetail> scanDetails,
122                        WifiConfiguration currentNetwork, String currentBssid,
123                        boolean connected, boolean untrustedNetworkAllowed,
124                        List<Pair<ScanDetail, WifiConfiguration>> connectableNetworks);
125    }
126
127    private final NetworkEvaluator[] mEvaluators = new NetworkEvaluator[MAX_NUM_EVALUATORS];
128
129    // A helper to log debugging information in the local log buffer, which can
130    // be retrieved in bugreport.
131    private void localLog(String log) {
132        mLocalLog.log(log);
133    }
134
135    private boolean isCurrentNetworkSufficient(WifiInfo wifiInfo, List<ScanDetail> scanDetails) {
136        WifiConfiguration network =
137                            mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
138
139        // Currently connected?
140        if (network == null) {
141            localLog("No current connected network.");
142            return false;
143        } else {
144            localLog("Current connected network: " + network.SSID
145                    + " , ID: " + network.networkId);
146        }
147
148        int currentRssi = wifiInfo.getRssi();
149        boolean hasQualifiedRssi = currentRssi
150                > mScoringParams.getSufficientRssi(wifiInfo.getFrequency());
151        boolean hasActiveStream = (wifiInfo.txSuccessRate > mStayOnNetworkMinimumTxRate)
152                || (wifiInfo.rxSuccessRate > mStayOnNetworkMinimumRxRate);
153        if (hasQualifiedRssi && hasActiveStream) {
154            localLog("Stay on current network because of good RSSI and ongoing traffic");
155            return true;
156        }
157
158        // Ephemeral network is not qualified.
159        if (network.ephemeral) {
160            localLog("Current network is an ephemeral one.");
161            return false;
162        }
163
164        // Open network is not qualified.
165        if (WifiConfigurationUtil.isConfigForOpenNetwork(network)) {
166            localLog("Current network is a open one.");
167            return false;
168        }
169
170        if (wifiInfo.is24GHz()) {
171            // 2.4GHz networks is not qualified whenever 5GHz is available
172            if (is5GHzNetworkAvailable(scanDetails)) {
173                localLog("Current network is 2.4GHz. 5GHz networks available.");
174                return false;
175            }
176        }
177        if (!hasQualifiedRssi) {
178            localLog("Current network RSSI[" + currentRssi + "]-acceptable but not qualified.");
179            return false;
180        }
181
182        // Network with no internet access reports is not qualified.
183        if (network.numNoInternetAccessReports > 0 && !network.noInternetAccessExpected) {
184            localLog("Current network has [" + network.numNoInternetAccessReports
185                    + "] no-internet access reports.");
186            return false;
187        }
188        return true;
189    }
190
191    // Determine whether there are any 5GHz networks in the scan result
192    private boolean is5GHzNetworkAvailable(List<ScanDetail> scanDetails) {
193        for (ScanDetail detail : scanDetails) {
194            ScanResult result = detail.getScanResult();
195            if (result.is5GHz()) return true;
196        }
197        return false;
198    }
199
200    private boolean isNetworkSelectionNeeded(List<ScanDetail> scanDetails, WifiInfo wifiInfo,
201                        boolean connected, boolean disconnected) {
202        if (scanDetails.size() == 0) {
203            localLog("Empty connectivity scan results. Skip network selection.");
204            return false;
205        }
206
207        if (connected) {
208            // Is roaming allowed?
209            if (!mEnableAutoJoinWhenAssociated) {
210                localLog("Switching networks in connected state is not allowed."
211                        + " Skip network selection.");
212                return false;
213            }
214
215            // Has it been at least the minimum interval since last network selection?
216            if (mLastNetworkSelectionTimeStamp != INVALID_TIME_STAMP) {
217                long gap = mClock.getElapsedSinceBootMillis()
218                            - mLastNetworkSelectionTimeStamp;
219                if (gap < MINIMUM_NETWORK_SELECTION_INTERVAL_MS) {
220                    localLog("Too short since last network selection: " + gap + " ms."
221                            + " Skip network selection.");
222                    return false;
223                }
224            }
225
226            if (isCurrentNetworkSufficient(wifiInfo, scanDetails)) {
227                localLog("Current connected network already sufficient. Skip network selection.");
228                return false;
229            } else {
230                localLog("Current connected network is not sufficient.");
231                return true;
232            }
233        } else if (disconnected) {
234            return true;
235        } else {
236            // No network selection if WifiStateMachine is in a state other than
237            // CONNECTED or DISCONNECTED.
238            localLog("WifiStateMachine is in neither CONNECTED nor DISCONNECTED state."
239                    + " Skip network selection.");
240            return false;
241        }
242    }
243
244    /**
245     * Format the given ScanResult as a scan ID for logging.
246     */
247    public static String toScanId(@Nullable ScanResult scanResult) {
248        return scanResult == null ? "NULL"
249                                  : String.format("%s:%s", scanResult.SSID, scanResult.BSSID);
250    }
251
252    /**
253     * Format the given WifiConfiguration as a SSID:netId string
254     */
255    public static String toNetworkString(WifiConfiguration network) {
256        if (network == null) {
257            return null;
258        }
259
260        return (network.SSID + ":" + network.networkId);
261    }
262
263    /**
264     * Compares ScanResult level against the minimum threshold for its band, returns true if lower
265     */
266    public boolean isSignalTooWeak(ScanResult scanResult) {
267        return (scanResult.level < mScoringParams.getEntryRssi(scanResult.frequency));
268    }
269
270    private List<ScanDetail> filterScanResults(List<ScanDetail> scanDetails,
271                HashSet<String> bssidBlacklist, boolean isConnected, String currentBssid) {
272        ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>();
273        List<ScanDetail> validScanDetails = new ArrayList<ScanDetail>();
274        StringBuffer noValidSsid = new StringBuffer();
275        StringBuffer blacklistedBssid = new StringBuffer();
276        StringBuffer lowRssi = new StringBuffer();
277        boolean scanResultsHaveCurrentBssid = false;
278
279        for (ScanDetail scanDetail : scanDetails) {
280            ScanResult scanResult = scanDetail.getScanResult();
281
282            if (TextUtils.isEmpty(scanResult.SSID)) {
283                noValidSsid.append(scanResult.BSSID).append(" / ");
284                continue;
285            }
286
287            // Check if the scan results contain the currently connected BSSID
288            if (scanResult.BSSID.equals(currentBssid)) {
289                scanResultsHaveCurrentBssid = true;
290            }
291
292            final String scanId = toScanId(scanResult);
293
294            if (bssidBlacklist.contains(scanResult.BSSID)) {
295                blacklistedBssid.append(scanId).append(" / ");
296                continue;
297            }
298
299            // Skip network with too weak signals.
300            if (isSignalTooWeak(scanResult)) {
301                lowRssi.append(scanId).append("(")
302                    .append(scanResult.is24GHz() ? "2.4GHz" : "5GHz")
303                    .append(")").append(scanResult.level).append(" / ");
304                continue;
305            }
306
307            validScanDetails.add(scanDetail);
308        }
309
310        // WNS listens to all single scan results. Some scan requests may not include
311        // the channel of the currently connected network, so the currently connected
312        // network won't show up in the scan results. We don't act on these scan results
313        // to avoid aggressive network switching which might trigger disconnection.
314        if (isConnected && !scanResultsHaveCurrentBssid) {
315            localLog("Current connected BSSID " + currentBssid + " is not in the scan results."
316                    + " Skip network selection.");
317            validScanDetails.clear();
318            return validScanDetails;
319        }
320
321        if (noValidSsid.length() != 0) {
322            localLog("Networks filtered out due to invalid SSID: " + noValidSsid);
323        }
324
325        if (blacklistedBssid.length() != 0) {
326            localLog("Networks filtered out due to blacklist: " + blacklistedBssid);
327        }
328
329        if (lowRssi.length() != 0) {
330            localLog("Networks filtered out due to low signal strength: " + lowRssi);
331        }
332
333        return validScanDetails;
334    }
335
336    /**
337     * This returns a list of ScanDetails that were filtered in the process of network selection.
338     * The list is further filtered for only open unsaved networks.
339     *
340     * @return the list of ScanDetails for open unsaved networks that do not have invalid SSIDS,
341     * blacklisted BSSIDS, or low signal strength. This will return an empty list when there are
342     * no open unsaved networks, or when network selection has not been run.
343     */
344    public List<ScanDetail> getFilteredScanDetailsForOpenUnsavedNetworks() {
345        List<ScanDetail> openUnsavedNetworks = new ArrayList<>();
346        for (ScanDetail scanDetail : mFilteredNetworks) {
347            ScanResult scanResult = scanDetail.getScanResult();
348
349            if (!ScanResultUtil.isScanResultForOpenNetwork(scanResult)) {
350                continue;
351            }
352
353            // Skip saved networks
354            if (mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail) != null) {
355                continue;
356            }
357
358            openUnsavedNetworks.add(scanDetail);
359        }
360        return openUnsavedNetworks;
361    }
362
363    /**
364     * This returns a list of ScanDetails that were filtered in the process of network selection.
365     * The list is further filtered for only carrier unsaved networks with EAP encryption.
366     *
367     * @param carrierConfig CarrierNetworkConfig used to filter carrier networks
368     * @return the list of ScanDetails for carrier unsaved networks that do not have invalid SSIDS,
369     * blacklisted BSSIDS, or low signal strength, and with EAP encryption. This will return an
370     * empty list when there are no such networks, or when network selection has not been run.
371     */
372    public List<ScanDetail> getFilteredScanDetailsForCarrierUnsavedNetworks(
373            CarrierNetworkConfig carrierConfig) {
374        List<ScanDetail> carrierUnsavedNetworks = new ArrayList<>();
375        for (ScanDetail scanDetail : mFilteredNetworks) {
376            ScanResult scanResult = scanDetail.getScanResult();
377
378            if (!ScanResultUtil.isScanResultForEapNetwork(scanResult)
379                    || !carrierConfig.isCarrierNetwork(scanResult.SSID)) {
380                continue;
381            }
382
383            // Skip saved networks
384            if (mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail) != null) {
385                continue;
386            }
387
388            carrierUnsavedNetworks.add(scanDetail);
389        }
390        return carrierUnsavedNetworks;
391    }
392
393    /**
394     * @return the list of ScanDetails scored as potential candidates by the last run of
395     * selectNetwork, this will be empty if Network selector determined no selection was
396     * needed on last run. This includes scan details of sufficient signal strength, and
397     * had an associated WifiConfiguration.
398     */
399    public List<Pair<ScanDetail, WifiConfiguration>> getConnectableScanDetails() {
400        return mConnectableNetworks;
401    }
402
403    /**
404     * This API is called when user explicitly selects a network. Currently, it is used in following
405     * cases:
406     * (1) User explicitly chooses to connect to a saved network.
407     * (2) User saves a network after adding a new network.
408     * (3) User saves a network after modifying a saved network.
409     * Following actions will be triggered:
410     * 1. If this network is disabled, we need re-enable it again.
411     * 2. This network is favored over all the other networks visible in latest network
412     *    selection procedure.
413     *
414     * @param netId  ID for the network chosen by the user
415     * @return true -- There is change made to connection choice of any saved network.
416     *         false -- There is no change made to connection choice of any saved network.
417     */
418    public boolean setUserConnectChoice(int netId) {
419        localLog("userSelectNetwork: network ID=" + netId);
420        WifiConfiguration selected = mWifiConfigManager.getConfiguredNetwork(netId);
421
422        if (selected == null || selected.SSID == null) {
423            localLog("userSelectNetwork: Invalid configuration with nid=" + netId);
424            return false;
425        }
426
427        // Enable the network if it is disabled.
428        if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) {
429            mWifiConfigManager.updateNetworkSelectionStatus(netId,
430                    WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE);
431        }
432
433        boolean change = false;
434        String key = selected.configKey();
435        // This is only used for setting the connect choice timestamp for debugging purposes.
436        long currentTime = mClock.getWallClockMillis();
437        List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks();
438
439        for (WifiConfiguration network : savedNetworks) {
440            WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus();
441            if (network.networkId == selected.networkId) {
442                if (status.getConnectChoice() != null) {
443                    localLog("Remove user selection preference of " + status.getConnectChoice()
444                            + " Set Time: " + status.getConnectChoiceTimestamp() + " from "
445                            + network.SSID + " : " + network.networkId);
446                    mWifiConfigManager.clearNetworkConnectChoice(network.networkId);
447                    change = true;
448                }
449                continue;
450            }
451
452            if (status.getSeenInLastQualifiedNetworkSelection()
453                    && (status.getConnectChoice() == null
454                    || !status.getConnectChoice().equals(key))) {
455                localLog("Add key: " + key + " Set Time: " + currentTime + " to "
456                        + toNetworkString(network));
457                mWifiConfigManager.setNetworkConnectChoice(network.networkId, key, currentTime);
458                change = true;
459            }
460        }
461
462        return change;
463    }
464
465    /**
466     * Overrides the {@code candidate} chosen by the {@link #mEvaluators} with the user chosen
467     * {@link WifiConfiguration} if one exists.
468     *
469     * @return the user chosen {@link WifiConfiguration} if one exists, {@code candidate} otherwise
470     */
471    private WifiConfiguration overrideCandidateWithUserConnectChoice(
472            @NonNull WifiConfiguration candidate) {
473        WifiConfiguration tempConfig = candidate;
474        WifiConfiguration originalCandidate = candidate;
475        ScanResult scanResultCandidate = candidate.getNetworkSelectionStatus().getCandidate();
476
477        while (tempConfig.getNetworkSelectionStatus().getConnectChoice() != null) {
478            String key = tempConfig.getNetworkSelectionStatus().getConnectChoice();
479            tempConfig = mWifiConfigManager.getConfiguredNetwork(key);
480
481            if (tempConfig != null) {
482                WifiConfiguration.NetworkSelectionStatus tempStatus =
483                        tempConfig.getNetworkSelectionStatus();
484                if (tempStatus.getCandidate() != null && tempStatus.isNetworkEnabled()) {
485                    scanResultCandidate = tempStatus.getCandidate();
486                    candidate = tempConfig;
487                }
488            } else {
489                localLog("Connect choice: " + key + " has no corresponding saved config.");
490                break;
491            }
492        }
493
494        if (candidate != originalCandidate) {
495            localLog("After user selection adjustment, the final candidate is:"
496                    + WifiNetworkSelector.toNetworkString(candidate) + " : "
497                    + scanResultCandidate.BSSID);
498        }
499        return candidate;
500    }
501
502    /**
503     * Select the best network from the ones in range.
504     *
505     * @param scanDetails    List of ScanDetail for all the APs in range
506     * @param bssidBlacklist Blacklisted BSSIDs
507     * @param wifiInfo       Currently connected network
508     * @param connected      True if the device is connected
509     * @param disconnected   True if the device is disconnected
510     * @param untrustedNetworkAllowed True if untrusted networks are allowed for connection
511     * @return Configuration of the selected network, or Null if nothing
512     */
513    @Nullable
514    public WifiConfiguration selectNetwork(List<ScanDetail> scanDetails,
515            HashSet<String> bssidBlacklist, WifiInfo wifiInfo,
516            boolean connected, boolean disconnected, boolean untrustedNetworkAllowed) {
517        mFilteredNetworks.clear();
518        mConnectableNetworks.clear();
519        if (scanDetails.size() == 0) {
520            localLog("Empty connectivity scan result");
521            return null;
522        }
523
524        WifiConfiguration currentNetwork =
525                mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
526
527        // Always get the current BSSID from WifiInfo in case that firmware initiated
528        // roaming happened.
529        String currentBssid = wifiInfo.getBSSID();
530
531        // Shall we start network selection at all?
532        if (!isNetworkSelectionNeeded(scanDetails, wifiInfo, connected, disconnected)) {
533            return null;
534        }
535
536        // Update the registered network evaluators.
537        for (NetworkEvaluator registeredEvaluator : mEvaluators) {
538            if (registeredEvaluator != null) {
539                registeredEvaluator.update(scanDetails);
540            }
541        }
542
543        // Filter out unwanted networks.
544        mFilteredNetworks = filterScanResults(scanDetails, bssidBlacklist,
545                connected, currentBssid);
546        if (mFilteredNetworks.size() == 0) {
547            return null;
548        }
549
550        // Go through the registered network evaluators from the highest priority
551        // one to the lowest till a network is selected.
552        WifiConfiguration selectedNetwork = null;
553        for (NetworkEvaluator registeredEvaluator : mEvaluators) {
554            if (registeredEvaluator != null) {
555                localLog("About to run " + registeredEvaluator.getName() + " :");
556                selectedNetwork = registeredEvaluator.evaluateNetworks(
557                        new ArrayList<>(mFilteredNetworks), currentNetwork, currentBssid, connected,
558                        untrustedNetworkAllowed, mConnectableNetworks);
559                if (selectedNetwork != null) {
560                    localLog(registeredEvaluator.getName() + " selects "
561                            + WifiNetworkSelector.toNetworkString(selectedNetwork) + " : "
562                            + selectedNetwork.getNetworkSelectionStatus().getCandidate().BSSID);
563                    break;
564                }
565            }
566        }
567
568        if (selectedNetwork != null) {
569            selectedNetwork = overrideCandidateWithUserConnectChoice(selectedNetwork);
570            mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis();
571        }
572
573        return selectedNetwork;
574    }
575
576    /**
577     * Register a network evaluator
578     *
579     * @param evaluator the network evaluator to be registered
580     * @param priority a value between 0 and (SCORER_MIN_PRIORITY-1)
581     *
582     * @return true if the evaluator is successfully registered with QNS;
583     *         false if failed to register the evaluator
584     */
585    public boolean registerNetworkEvaluator(NetworkEvaluator evaluator, int priority) {
586        if (priority < 0 || priority >= EVALUATOR_MIN_PRIORITY) {
587            localLog("Invalid network evaluator priority: " + priority);
588            return false;
589        }
590
591        if (mEvaluators[priority] != null) {
592            localLog("Priority " + priority + " is already registered by "
593                    + mEvaluators[priority].getName());
594            return false;
595        }
596
597        mEvaluators[priority] = evaluator;
598        return true;
599    }
600
601    WifiNetworkSelector(Context context, ScoringParams scoringParams,
602            WifiConfigManager configManager, Clock clock,
603            LocalLog localLog) {
604        mWifiConfigManager = configManager;
605        mClock = clock;
606        mScoringParams = scoringParams;
607        mLocalLog = localLog;
608
609        mEnableAutoJoinWhenAssociated = context.getResources().getBoolean(
610                R.bool.config_wifi_framework_enable_associated_network_selection);
611        mStayOnNetworkMinimumTxRate = context.getResources().getInteger(
612                R.integer.config_wifi_framework_min_tx_rate_for_staying_on_network);
613        mStayOnNetworkMinimumRxRate = context.getResources().getInteger(
614                R.integer.config_wifi_framework_min_rx_rate_for_staying_on_network);
615    }
616}
617