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