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