WifiNetworkSelector.java revision b2213b373794719cb1377c3b68e58578fbd610da
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 WifiInfo mWifiInfo;
60    private Clock mClock;
61    private WifiConfiguration mCurrentNetwork = null;
62    private String mCurrentBssid = null;
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(WifiConfiguration network) {
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 (mWifiInfo.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 = mWifiInfo.getRssi();
186        if (mWifiInfo.is5GHz() && currentRssi < mThresholdQualifiedRssi5) {
187            localLog("Current network band=" + (mWifiInfo.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,
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(mCurrentNetwork)) {
222                localLog("Current connected network already sufficient. Skip network selection.");
223                return false;
224            } else {
225                localLog("Current connected network is not sufficient.");
226            }
227        } else if (disconnected) {
228            mCurrentNetwork = null;
229            mCurrentBssid = null;
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        return true;
239    }
240
241    /**
242     * Format the given ScanResult as a scan ID for logging.
243     */
244    public static String toScanId(@Nullable ScanResult scanResult) {
245        return scanResult == null ? "NULL"
246                                  : String.format("%s:%s", scanResult.SSID, scanResult.BSSID);
247    }
248
249    /**
250     * Format the given WifiConfiguration as a SSID:netId string
251     */
252    public static String toNetworkString(WifiConfiguration network) {
253        if (network == null) {
254            return null;
255        }
256
257        return (network.SSID + ":" + network.networkId);
258    }
259
260    private List<ScanDetail> filterScanResults(List<ScanDetail> scanDetails) {
261        ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>();
262        List<ScanDetail> validScanDetails = new ArrayList<ScanDetail>();
263        StringBuffer noValidSsid = new StringBuffer();
264        StringBuffer blacklistedBssid = new StringBuffer();
265        StringBuffer lowRssi = new StringBuffer();
266
267        for (ScanDetail scanDetail : scanDetails) {
268            ScanResult scanResult = scanDetail.getScanResult();
269
270            if (TextUtils.isEmpty(scanResult.SSID)) {
271                noValidSsid.append(scanResult.BSSID).append(" / ");
272                continue;
273            }
274
275            final String scanId = toScanId(scanResult);
276
277            if (isBssidDisabled(scanResult.BSSID)) {
278                blacklistedBssid.append(scanId).append(" / ");
279                continue;
280            }
281
282            // Skip network with too weak signals.
283            if ((scanResult.is24GHz() && scanResult.level
284                    < mThresholdMinimumRssi24)
285                    || (scanResult.is5GHz() && scanResult.level
286                    < mThresholdMinimumRssi5)) {
287                lowRssi.append(scanId).append("(")
288                    .append(scanResult.is24GHz() ? "2.4GHz" : "5GHz")
289                    .append(")").append(scanResult.level).append(" / ");
290                continue;
291            }
292
293            validScanDetails.add(scanDetail);
294        }
295
296        if (noValidSsid.length() != 0) {
297            localLog("Networks filtered out due to invalid SSID: " + noValidSsid);
298        }
299
300        if (blacklistedBssid.length() != 0) {
301            localLog("Networks filtered out due to blacklist: " + blacklistedBssid);
302        }
303
304        if (lowRssi.length() != 0) {
305            localLog("Networks filtered out due to low signal strength: " + lowRssi);
306        }
307
308        return validScanDetails;
309    }
310
311    /**
312     * @return the list of ScanDetails scored as potential candidates by the last run of
313     * selectNetwork, this will be empty if Network selector determined no selection was
314     * needed on last run. This includes scan details of sufficient signal strength, and
315     * had an associated WifiConfiguration.
316     */
317    public List<Pair<ScanDetail, WifiConfiguration>> getFilteredScanDetails() {
318        return mConnectableNetworks;
319    }
320
321    /**
322     * This API is called when user explicitly selects a network. Currently, it is used in following
323     * cases:
324     * (1) User explicitly chooses to connect to a saved network.
325     * (2) User saves a network after adding a new network.
326     * (3) User saves a network after modifying a saved network.
327     * Following actions will be triggered:
328     * 1. If this network is disabled, we need re-enable it again.
329     * 2. This network is favored over all the other networks visible in latest network
330     *    selection procedure.
331     *
332     * @param netId  ID for the network chosen by the user
333     * @return true -- There is change made to connection choice of any saved network.
334     *         false -- There is no change made to connection choice of any saved network.
335     */
336    public boolean setUserConnectChoice(int netId) {
337        localLog("userSelectNetwork: network ID=" + netId);
338        WifiConfiguration selected = mWifiConfigManager.getConfiguredNetwork(netId);
339
340        if (selected == null || selected.SSID == null) {
341            localLog("userSelectNetwork: Invalid configuration with nid=" + netId);
342            return false;
343        }
344
345        // Enable the network if it is disabled.
346        if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) {
347            mWifiConfigManager.updateNetworkSelectionStatus(netId,
348                    WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE);
349        }
350
351        boolean change = false;
352        String key = selected.configKey();
353        // This is only used for setting the connect choice timestamp for debugging purposes.
354        long currentTime = mClock.getWallClockMillis();
355        List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks();
356
357        for (WifiConfiguration network : savedNetworks) {
358            WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus();
359            if (network.networkId == selected.networkId) {
360                if (status.getConnectChoice() != null) {
361                    localLog("Remove user selection preference of " + status.getConnectChoice()
362                            + " Set Time: " + status.getConnectChoiceTimestamp() + " from "
363                            + network.SSID + " : " + network.networkId);
364                    mWifiConfigManager.clearNetworkConnectChoice(network.networkId);
365                    change = true;
366                }
367                continue;
368            }
369
370            if (status.getSeenInLastQualifiedNetworkSelection()
371                    && (status.getConnectChoice() == null
372                    || !status.getConnectChoice().equals(key))) {
373                localLog("Add key: " + key + " Set Time: " + currentTime + " to "
374                        + toNetworkString(network));
375                mWifiConfigManager.setNetworkConnectChoice(network.networkId, key, currentTime);
376                change = true;
377            }
378        }
379
380        return change;
381    }
382
383    /**
384     * Enable/disable a BSSID for Network Selection
385     * When an association rejection event is obtained, Network Selector will disable this
386     * BSSID but supplicant still can try to connect to this bssid. If supplicant connect to it
387     * successfully later, this bssid can be re-enabled.
388     *
389     * @param bssid the bssid to be enabled / disabled
390     * @param enable -- true enable a bssid if it has been disabled
391     *               -- false disable a bssid
392     */
393    public boolean enableBssidForNetworkSelection(String bssid, boolean enable) {
394        if (enable) {
395            return (mBssidBlacklist.remove(bssid) != null);
396        } else {
397            if (bssid != null) {
398                BssidBlacklistStatus status = mBssidBlacklist.get(bssid);
399                if (status == null) {
400                    // First time for this BSSID
401                    BssidBlacklistStatus newStatus = new BssidBlacklistStatus();
402                    newStatus.counter++;
403                    mBssidBlacklist.put(bssid, newStatus);
404                } else if (!status.isBlacklisted) {
405                    status.counter++;
406                    if (status.counter >= BSSID_BLACKLIST_THRESHOLD) {
407                        status.isBlacklisted = true;
408                        status.blacklistedTimeStamp = mClock.getElapsedSinceBootMillis();
409                        return true;
410                    }
411                }
412            }
413        }
414        return false;
415    }
416
417    /**
418     * Update the BSSID blacklist
419     *
420     * Go through the BSSID blacklist and check when a BSSID was blocked. If it
421     * has been blacklisted for BSSID_BLACKLIST_EXPIRE_TIME_MS, then re-enable it.
422     */
423    private void updateBssidBlacklist() {
424        Iterator<BssidBlacklistStatus> iter = mBssidBlacklist.values().iterator();
425        while (iter.hasNext()) {
426            BssidBlacklistStatus status = iter.next();
427            if (status != null && status.isBlacklisted) {
428                if (mClock.getElapsedSinceBootMillis() - status.blacklistedTimeStamp
429                            >= BSSID_BLACKLIST_EXPIRE_TIME_MS) {
430                    iter.remove();
431                }
432            }
433        }
434    }
435
436    /**
437     * Check whether a bssid is disabled
438     * @param bssid -- the bssid to check
439     */
440    private boolean isBssidDisabled(String bssid) {
441        BssidBlacklistStatus status = mBssidBlacklist.get(bssid);
442        return status == null ? false : status.isBlacklisted;
443    }
444
445    /**
446     *
447     */
448    @Nullable
449    public WifiConfiguration selectNetwork(List<ScanDetail> scanDetails,
450            boolean connected, boolean disconnected, boolean untrustedNetworkAllowed) {
451        mConnectableNetworks.clear();
452        if (scanDetails.size() == 0) {
453            localLog("Empty connectivity scan result");
454            return null;
455        }
456
457        if (mCurrentNetwork == null) {
458            mCurrentNetwork =
459                mWifiConfigManager.getConfiguredNetwork(mWifiInfo.getNetworkId());
460        }
461
462        // Always get the current BSSID from WifiInfo in case that firmware initiated
463        // roaming happened.
464        mCurrentBssid = mWifiInfo.getBSSID();
465
466        // Shall we start network selection at all?
467        if (!isNetworkSelectionNeeded(scanDetails, connected, disconnected)) {
468            return null;
469        }
470
471        // Update the registered network evaluators.
472        for (NetworkEvaluator registeredEvaluator : mEvaluators) {
473            if (registeredEvaluator != null) {
474                registeredEvaluator.update(scanDetails);
475            }
476        }
477
478        // Check if any BSSID can be freed from the blacklist.
479        updateBssidBlacklist();
480
481        // Filter out unwanted networks.
482        List<ScanDetail> filteredScanDetails = filterScanResults(scanDetails);
483        if (filteredScanDetails.size() == 0) {
484            return null;
485        }
486
487        // Go through the registered network evaluators from the highest priority
488        // one to the lowest till a network is selected.
489        WifiConfiguration selectedNetwork = null;
490        for (NetworkEvaluator registeredEvaluator : mEvaluators) {
491            if (registeredEvaluator != null) {
492                selectedNetwork = registeredEvaluator.evaluateNetworks(scanDetails,
493                            mCurrentNetwork, mCurrentBssid, connected,
494                            untrustedNetworkAllowed, mConnectableNetworks);
495                if (selectedNetwork != null) {
496                    break;
497                }
498            }
499        }
500
501        if (selectedNetwork != null) {
502            mCurrentNetwork = selectedNetwork;
503            mCurrentBssid = selectedNetwork.getNetworkSelectionStatus().getCandidate().BSSID;
504            mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis();
505        }
506
507        return selectedNetwork;
508    }
509
510    /**
511     * Register a network evaluator
512     *
513     * @param evaluator the network evaluator to be registered
514     * @param priority a value between 0 and (SCORER_MIN_PRIORITY-1)
515     *
516     * @return true if the evaluator is successfully registered with QNS;
517     *         false if failed to register the evaluator
518     */
519    public boolean registerNetworkEvaluator(NetworkEvaluator evaluator, int priority) {
520        if (priority < 0 || priority >= EVALUATOR_MIN_PRIORITY) {
521            Log.e(TAG, "Invalid network evaluator priority: " + priority);
522            return false;
523        }
524
525        if (mEvaluators[priority] != null) {
526            Log.e(TAG, "Priority " + priority + " is already registered by "
527                    + mEvaluators[priority].getName());
528            return false;
529        }
530
531        mEvaluators[priority] = evaluator;
532        return true;
533    }
534
535    /**
536     * Unregister a network evaluator
537     *
538     * @param evaluator the network evaluator to be unregistered from QNS
539     *
540     * @return true if the evaluator is successfully unregistered from;
541     *         false if failed to unregister the evaluator
542     */
543    public boolean unregisterNetworkEvaluator(NetworkEvaluator evaluator) {
544        for (NetworkEvaluator registeredEvaluator : mEvaluators) {
545            if (registeredEvaluator == evaluator) {
546                Log.d(TAG, "Unregistered network evaluator: " + evaluator.getName());
547                return true;
548            }
549        }
550
551        Log.e(TAG, "Couldn't unregister network evaluator: " + evaluator.getName());
552        return false;
553    }
554
555    WifiNetworkSelector(Context context, WifiConfigManager configManager,
556            WifiInfo wifiInfo, Clock clock) {
557        mWifiConfigManager = configManager;
558        mWifiInfo = wifiInfo;
559        mClock = clock;
560
561        mThresholdQualifiedRssi24 = context.getResources().getInteger(
562                            R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_24GHz);
563        mThresholdQualifiedRssi5 = context.getResources().getInteger(
564                            R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_5GHz);
565        mThresholdMinimumRssi24 = context.getResources().getInteger(
566                            R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_24GHz);
567        mThresholdMinimumRssi5 = context.getResources().getInteger(
568                            R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_5GHz);
569        mEnableAutoJoinWhenAssociated = context.getResources().getBoolean(
570                            R.bool.config_wifi_framework_enable_associated_network_selection);
571    }
572
573    /**
574     * Retrieve the local log buffer created by WifiNetworkSelector.
575     */
576    public LocalLog getLocalLog() {
577        return mLocalLog;
578    }
579
580    /**
581     * Dump the local logs.
582     */
583    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
584        pw.println("Dump of WifiNetworkSelector");
585        pw.println("WifiNetworkSelector - Log Begin ----");
586        mLocalLog.dump(fd, pw, args);
587        pw.println("WifiNetworkSelector - Log End ----");
588    }
589}
590