WifiNetworkSelector.java revision 235642dba4359c1e68618f27c949e744765cbbcc
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.app.ActivityManager;
22import android.content.Context;
23import android.net.NetworkKey;
24import android.net.wifi.ScanResult;
25import android.net.wifi.WifiConfiguration;
26import android.net.wifi.WifiInfo;
27import android.text.TextUtils;
28import android.util.LocalLog;
29import android.util.Pair;
30
31import com.android.internal.R;
32import com.android.internal.annotations.VisibleForTesting;
33
34import java.io.FileDescriptor;
35import java.io.PrintWriter;
36import java.util.ArrayList;
37import java.util.HashMap;
38import java.util.Iterator;
39import java.util.List;
40import java.util.Map;
41
42/**
43 * This class looks at all the connectivity scan results then
44 * selects a network for the phone to connect or roam to.
45 */
46public class WifiNetworkSelector {
47    private static final long INVALID_TIME_STAMP = Long.MIN_VALUE;
48    // Minimum time gap between last successful network selection and a new selection
49    // attempt.
50    @VisibleForTesting
51    public static final int MINIMUM_NETWORK_SELECTION_INTERVAL_MS = 10 * 1000;
52
53    // Constants for BSSID blacklist.
54    public static final int BSSID_BLACKLIST_THRESHOLD = 3;
55    public static final int BSSID_BLACKLIST_EXPIRE_TIME_MS = 5 * 60 * 1000;
56
57    // Association success/failure reason codes
58    @VisibleForTesting
59    public static final int REASON_CODE_AP_UNABLE_TO_HANDLE_NEW_STA = 17;
60
61    private WifiConfigManager mWifiConfigManager;
62    private Clock mClock;
63    private static class BssidBlacklistStatus {
64        // Number of times this BSSID has been rejected for association.
65        public int counter;
66        public boolean isBlacklisted;
67        public long blacklistedTimeStamp = INVALID_TIME_STAMP;
68    }
69    private Map<String, BssidBlacklistStatus> mBssidBlacklist =
70            new HashMap<>();
71
72    private final LocalLog mLocalLog =
73            new LocalLog(ActivityManager.isLowRamDeviceStatic() ? 256 : 512);
74    private long mLastNetworkSelectionTimeStamp = INVALID_TIME_STAMP;
75    // Buffer of filtered scan results (Scan results considered by network selection) & associated
76    // WifiConfiguration (if any).
77    private volatile List<Pair<ScanDetail, WifiConfiguration>> mConnectableNetworks =
78            new ArrayList<>();
79    private final int mThresholdQualifiedRssi24;
80    private final int mThresholdQualifiedRssi5;
81    private final int mThresholdMinimumRssi24;
82    private final int mThresholdMinimumRssi5;
83    private final boolean mEnableAutoJoinWhenAssociated;
84
85    /**
86     * WiFi Network Selector supports various types of networks. Each type can
87     * have its evaluator to choose the best WiFi network for the device to connect
88     * to. When registering a WiFi network evaluator with the WiFi Network Selector,
89     * the priority of the network must be specified, and it must be a value between
90     * 0 and (EVALUATOR_MIN_PIRORITY - 1) with 0 being the highest priority. Wifi
91     * Network Selector iterates through the registered scorers from the highest priority
92     * to the lowest till a network is selected.
93     */
94    public static final int EVALUATOR_MIN_PRIORITY = 6;
95
96    /**
97     * Maximum number of evaluators can be registered with Wifi Network Selector.
98     */
99    public static final int MAX_NUM_EVALUATORS = EVALUATOR_MIN_PRIORITY;
100
101    /**
102     * Interface for WiFi Network Evaluator
103     *
104     * A network scorer evaulates all the networks from the scan results and
105     * recommends the best network in its category to connect or roam to.
106     */
107    public interface NetworkEvaluator {
108        /**
109         * Get the evaluator name.
110         */
111        String getName();
112
113        /**
114         * Update the evaluator.
115         *
116         * Certain evaluators have to be updated with the new scan results. For example
117         * the ExternalScoreEvalutor needs to refresh its Score Cache.
118         *
119         * @param scanDetails    a list of scan details constructed from the scan results
120         */
121        void update(List<ScanDetail> scanDetails);
122
123        /**
124         * Evaluate all the networks from the scan results.
125         *
126         * @param scanDetails    a list of scan details constructed from the scan results
127         * @param currentNetwork configuration of the current connected network
128         *                       or null if disconnected
129         * @param currentBssid   BSSID of the current connected network or null if
130         *                       disconnected
131         * @param connected      a flag to indicate if WifiStateMachine is in connected
132         *                       state
133         * @param untrustedNetworkAllowed a flag to indidate if untrusted networks like
134         *                                ephemeral networks are allowed
135         * @param connectableNetworks     a list of the ScanDetail and WifiConfiguration
136         *                                pair which is used by the WifiLastResortWatchdog
137         * @return configuration of the chosen network;
138         *         null if no network in this category is available.
139         */
140        @Nullable
141        WifiConfiguration evaluateNetworks(List<ScanDetail> scanDetails,
142                        WifiConfiguration currentNetwork, String currentBssid,
143                        boolean connected, boolean untrustedNetworkAllowed,
144                        List<Pair<ScanDetail, WifiConfiguration>> connectableNetworks);
145    }
146
147    private final NetworkEvaluator[] mEvaluators = new NetworkEvaluator[MAX_NUM_EVALUATORS];
148
149    // A helper to log debugging information in the local log buffer, which can
150    // be retrieved in bugreport.
151    private void localLog(String log) {
152        mLocalLog.log(log);
153    }
154
155    private boolean isCurrentNetworkSufficient(WifiInfo wifiInfo) {
156        WifiConfiguration network =
157                            mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
158
159        // Currently connected?
160        if (network == null) {
161            localLog("No current connected network.");
162            return false;
163        } else {
164            localLog("Current connected network: " + network.SSID
165                    + " , ID: " + network.networkId);
166        }
167
168        // Ephemeral network is not qualified.
169        if (network.ephemeral) {
170            localLog("Current network is an ephemeral one.");
171            return false;
172        }
173
174        // Open network is not qualified.
175        if (WifiConfigurationUtil.isConfigForOpenNetwork(network)) {
176            localLog("Current network is a open one.");
177            return false;
178        }
179
180        // 2.4GHz networks is not qualified.
181        if (wifiInfo.is24GHz()) {
182            localLog("Current network is 2.4GHz.");
183            return false;
184        }
185
186        // Is the current network's singnal strength qualified? It can only
187        // be a 5GHz network if we reach here.
188        int currentRssi = wifiInfo.getRssi();
189        if (wifiInfo.is5GHz() && currentRssi < mThresholdQualifiedRssi5) {
190            localLog("Current network band=" + (wifiInfo.is5GHz() ? "5GHz" : "2.4GHz")
191                    + ", RSSI[" + currentRssi + "]-acceptable but not qualified.");
192            return false;
193        }
194
195        return true;
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)) {
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, boolean isConnected,
262                    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 (isBssidDisabled(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     * @return the list of ScanDetails scored as potential candidates by the last run of
332     * selectNetwork, this will be empty if Network selector determined no selection was
333     * needed on last run. This includes scan details of sufficient signal strength, and
334     * had an associated WifiConfiguration.
335     */
336    public List<Pair<ScanDetail, WifiConfiguration>> getFilteredScanDetails() {
337        return mConnectableNetworks;
338    }
339
340    /**
341     * This API is called when user explicitly selects a network. Currently, it is used in following
342     * cases:
343     * (1) User explicitly chooses to connect to a saved network.
344     * (2) User saves a network after adding a new network.
345     * (3) User saves a network after modifying a saved network.
346     * Following actions will be triggered:
347     * 1. If this network is disabled, we need re-enable it again.
348     * 2. This network is favored over all the other networks visible in latest network
349     *    selection procedure.
350     *
351     * @param netId  ID for the network chosen by the user
352     * @return true -- There is change made to connection choice of any saved network.
353     *         false -- There is no change made to connection choice of any saved network.
354     */
355    public boolean setUserConnectChoice(int netId) {
356        localLog("userSelectNetwork: network ID=" + netId);
357        WifiConfiguration selected = mWifiConfigManager.getConfiguredNetwork(netId);
358
359        if (selected == null || selected.SSID == null) {
360            localLog("userSelectNetwork: Invalid configuration with nid=" + netId);
361            return false;
362        }
363
364        // Enable the network if it is disabled.
365        if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) {
366            mWifiConfigManager.updateNetworkSelectionStatus(netId,
367                    WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE);
368        }
369
370        boolean change = false;
371        String key = selected.configKey();
372        // This is only used for setting the connect choice timestamp for debugging purposes.
373        long currentTime = mClock.getWallClockMillis();
374        List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks();
375
376        for (WifiConfiguration network : savedNetworks) {
377            WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus();
378            if (network.networkId == selected.networkId) {
379                if (status.getConnectChoice() != null) {
380                    localLog("Remove user selection preference of " + status.getConnectChoice()
381                            + " Set Time: " + status.getConnectChoiceTimestamp() + " from "
382                            + network.SSID + " : " + network.networkId);
383                    mWifiConfigManager.clearNetworkConnectChoice(network.networkId);
384                    change = true;
385                }
386                continue;
387            }
388
389            if (status.getSeenInLastQualifiedNetworkSelection()
390                    && (status.getConnectChoice() == null
391                    || !status.getConnectChoice().equals(key))) {
392                localLog("Add key: " + key + " Set Time: " + currentTime + " to "
393                        + toNetworkString(network));
394                mWifiConfigManager.setNetworkConnectChoice(network.networkId, key, currentTime);
395                change = true;
396            }
397        }
398
399        return change;
400    }
401
402    /**
403     * Overrides the {@code candidate} chosen by the {@link #mEvaluators} with the user chosen
404     * {@link WifiConfiguration} if one exists.
405     *
406     * @return the user chosen {@link WifiConfiguration} if one exists, {@code candidate} otherwise
407     */
408    private WifiConfiguration overrideCandidateWithUserConnectChoice(
409            @NonNull WifiConfiguration candidate) {
410        WifiConfiguration tempConfig = candidate;
411        ScanResult scanResultCandidate = candidate.getNetworkSelectionStatus().getCandidate();
412
413        while (tempConfig.getNetworkSelectionStatus().getConnectChoice() != null) {
414            String key = tempConfig.getNetworkSelectionStatus().getConnectChoice();
415            tempConfig = mWifiConfigManager.getConfiguredNetwork(key);
416
417            if (tempConfig != null) {
418                WifiConfiguration.NetworkSelectionStatus tempStatus =
419                        tempConfig.getNetworkSelectionStatus();
420                if (tempStatus.getCandidate() != null && tempStatus.isNetworkEnabled()) {
421                    scanResultCandidate = tempStatus.getCandidate();
422                    candidate = tempConfig;
423                }
424            } else {
425                localLog("Connect choice: " + key + " has no corresponding saved config.");
426                break;
427            }
428        }
429        localLog("After user selection adjustment, the final candidate is:"
430                + WifiNetworkSelector.toNetworkString(candidate) + " : "
431                + scanResultCandidate.BSSID);
432        return candidate;
433    }
434
435    /**
436     * Enable/disable a BSSID for Network Selection
437     * When an association rejection event is obtained, Network Selector will disable this
438     * BSSID but supplicant still can try to connect to this bssid. If supplicant connect to it
439     * successfully later, this bssid can be re-enabled.
440     *
441     * @param bssid the bssid to be enabled / disabled
442     * @param enable -- true enable a bssid if it has been disabled
443     *               -- false disable a bssid
444     * @param reasonCode enable/disable reason code
445     */
446    public boolean enableBssidForNetworkSelection(String bssid, boolean enable, int reasonCode) {
447        if (enable) {
448            return (mBssidBlacklist.remove(bssid) != null);
449        } else {
450            if (bssid != null) {
451                BssidBlacklistStatus status = mBssidBlacklist.get(bssid);
452                if (status == null) {
453                    // First time for this BSSID
454                    status = new BssidBlacklistStatus();
455                    mBssidBlacklist.put(bssid, status);
456                }
457
458                if (!status.isBlacklisted) {
459                    status.counter++;
460                    if (status.counter >= BSSID_BLACKLIST_THRESHOLD
461                            || reasonCode == REASON_CODE_AP_UNABLE_TO_HANDLE_NEW_STA) {
462                        status.isBlacklisted = true;
463                        status.blacklistedTimeStamp = mClock.getElapsedSinceBootMillis();
464                        return true;
465                    }
466                }
467            }
468        }
469        return false;
470    }
471
472    /**
473     * Update the BSSID blacklist
474     *
475     * Go through the BSSID blacklist and check when a BSSID was blocked. If it
476     * has been blacklisted for BSSID_BLACKLIST_EXPIRE_TIME_MS, then re-enable it.
477     */
478    private void updateBssidBlacklist() {
479        Iterator<BssidBlacklistStatus> iter = mBssidBlacklist.values().iterator();
480        while (iter.hasNext()) {
481            BssidBlacklistStatus status = iter.next();
482            if (status != null && status.isBlacklisted) {
483                if (mClock.getElapsedSinceBootMillis() - status.blacklistedTimeStamp
484                            >= BSSID_BLACKLIST_EXPIRE_TIME_MS) {
485                    iter.remove();
486                }
487            }
488        }
489    }
490
491    /**
492     * Check whether a bssid is disabled
493     * @param bssid -- the bssid to check
494     */
495    private boolean isBssidDisabled(String bssid) {
496        BssidBlacklistStatus status = mBssidBlacklist.get(bssid);
497        return status == null ? false : status.isBlacklisted;
498    }
499
500    /**
501     *
502     */
503    @Nullable
504    public WifiConfiguration selectNetwork(List<ScanDetail> scanDetails, WifiInfo wifiInfo,
505            boolean connected, boolean disconnected, boolean untrustedNetworkAllowed) {
506        mConnectableNetworks.clear();
507        if (scanDetails.size() == 0) {
508            localLog("Empty connectivity scan result");
509            return null;
510        }
511
512        WifiConfiguration currentNetwork =
513                mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
514
515        // Always get the current BSSID from WifiInfo in case that firmware initiated
516        // roaming happened.
517        String currentBssid = wifiInfo.getBSSID();
518
519        // Shall we start network selection at all?
520        if (!isNetworkSelectionNeeded(scanDetails, wifiInfo, connected, disconnected)) {
521            return null;
522        }
523
524        // Update the registered network evaluators.
525        for (NetworkEvaluator registeredEvaluator : mEvaluators) {
526            if (registeredEvaluator != null) {
527                registeredEvaluator.update(scanDetails);
528            }
529        }
530
531        // Check if any BSSID can be freed from the blacklist.
532        updateBssidBlacklist();
533
534        // Filter out unwanted networks.
535        List<ScanDetail> filteredScanDetails = filterScanResults(scanDetails, connected,
536                currentBssid);
537        if (filteredScanDetails.size() == 0) {
538            return null;
539        }
540
541        // Go through the registered network evaluators from the highest priority
542        // one to the lowest till a network is selected.
543        WifiConfiguration selectedNetwork = null;
544        for (NetworkEvaluator registeredEvaluator : mEvaluators) {
545            if (registeredEvaluator != null) {
546                selectedNetwork = registeredEvaluator.evaluateNetworks(filteredScanDetails,
547                            currentNetwork, currentBssid, connected,
548                            untrustedNetworkAllowed, mConnectableNetworks);
549                if (selectedNetwork != null) {
550                    break;
551                }
552            }
553        }
554
555        if (selectedNetwork != null) {
556            selectedNetwork = overrideCandidateWithUserConnectChoice(selectedNetwork);
557            mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis();
558        }
559
560        return selectedNetwork;
561    }
562
563    /**
564     * Register a network evaluator
565     *
566     * @param evaluator the network evaluator to be registered
567     * @param priority a value between 0 and (SCORER_MIN_PRIORITY-1)
568     *
569     * @return true if the evaluator is successfully registered with QNS;
570     *         false if failed to register the evaluator
571     */
572    public boolean registerNetworkEvaluator(NetworkEvaluator evaluator, int priority) {
573        if (priority < 0 || priority >= EVALUATOR_MIN_PRIORITY) {
574            localLog("Invalid network evaluator priority: " + priority);
575            return false;
576        }
577
578        if (mEvaluators[priority] != null) {
579            localLog("Priority " + priority + " is already registered by "
580                    + mEvaluators[priority].getName());
581            return false;
582        }
583
584        mEvaluators[priority] = evaluator;
585        return true;
586    }
587
588    WifiNetworkSelector(Context context, WifiConfigManager configManager, Clock clock) {
589        mWifiConfigManager = configManager;
590        mClock = clock;
591
592        mThresholdQualifiedRssi24 = context.getResources().getInteger(
593                            R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_24GHz);
594        mThresholdQualifiedRssi5 = context.getResources().getInteger(
595                            R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_5GHz);
596        mThresholdMinimumRssi24 = context.getResources().getInteger(
597                            R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_24GHz);
598        mThresholdMinimumRssi5 = context.getResources().getInteger(
599                            R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_5GHz);
600        mEnableAutoJoinWhenAssociated = context.getResources().getBoolean(
601                            R.bool.config_wifi_framework_enable_associated_network_selection);
602    }
603
604    /**
605     * Retrieve the local log buffer created by WifiNetworkSelector.
606     */
607    public LocalLog getLocalLog() {
608        return mLocalLog;
609    }
610
611    /**
612     * Dump the local logs.
613     */
614    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
615        pw.println("Dump of WifiNetworkSelector");
616        pw.println("WifiNetworkSelector - Log Begin ----");
617        mLocalLog.dump(fd, pw, args);
618        pw.println("WifiNetworkSelector - Log End ----");
619    }
620}
621