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