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