WifiNetworkSelector.java revision 17c2a7b30e5680b11fc0073ce322ee7bc14ef2c5
1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.wifi;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.Context;
22import android.net.NetworkKey;
23import android.net.wifi.ScanResult;
24import android.net.wifi.WifiConfiguration;
25import android.net.wifi.WifiInfo;
26import android.text.TextUtils;
27import android.util.LocalLog;
28import android.util.Pair;
29
30import com.android.internal.R;
31import com.android.internal.annotations.VisibleForTesting;
32
33import java.util.ArrayList;
34import java.util.HashSet;
35import java.util.List;
36
37/**
38 * This class looks at all the connectivity scan results then
39 * selects a network for the phone to connect or roam to.
40 */
41public class WifiNetworkSelector {
42    private static final long INVALID_TIME_STAMP = Long.MIN_VALUE;
43    // Minimum time gap between last successful network selection and a new selection
44    // attempt.
45    @VisibleForTesting
46    public static final int MINIMUM_NETWORK_SELECTION_INTERVAL_MS = 10 * 1000;
47
48    private final WifiConfigManager mWifiConfigManager;
49    private final Clock mClock;
50    private final LocalLog mLocalLog;
51    private long mLastNetworkSelectionTimeStamp = INVALID_TIME_STAMP;
52    // Buffer of filtered scan results (Scan results considered by network selection) & associated
53    // WifiConfiguration (if any).
54    private volatile List<Pair<ScanDetail, WifiConfiguration>> mConnectableNetworks =
55            new ArrayList<>();
56    private final int mThresholdQualifiedRssi24;
57    private final int mThresholdQualifiedRssi5;
58    private final int mThresholdMinimumRssi24;
59    private final int mThresholdMinimumRssi5;
60    private final boolean mEnableAutoJoinWhenAssociated;
61
62    /**
63     * WiFi Network Selector supports various types of networks. Each type can
64     * have its evaluator to choose the best WiFi network for the device to connect
65     * to. When registering a WiFi network evaluator with the WiFi Network Selector,
66     * the priority of the network must be specified, and it must be a value between
67     * 0 and (EVALUATOR_MIN_PIRORITY - 1) with 0 being the highest priority. Wifi
68     * Network Selector iterates through the registered scorers from the highest priority
69     * to the lowest till a network is selected.
70     */
71    public static final int EVALUATOR_MIN_PRIORITY = 6;
72
73    /**
74     * Maximum number of evaluators can be registered with Wifi Network Selector.
75     */
76    public static final int MAX_NUM_EVALUATORS = EVALUATOR_MIN_PRIORITY;
77
78    /**
79     * Interface for WiFi Network Evaluator
80     *
81     * A network scorer evaulates all the networks from the scan results and
82     * recommends the best network in its category to connect or roam to.
83     */
84    public interface NetworkEvaluator {
85        /**
86         * Get the evaluator name.
87         */
88        String getName();
89
90        /**
91         * Update the evaluator.
92         *
93         * Certain evaluators have to be updated with the new scan results. For example
94         * the ExternalScoreEvalutor needs to refresh its Score Cache.
95         *
96         * @param scanDetails    a list of scan details constructed from the scan results
97         */
98        void update(List<ScanDetail> scanDetails);
99
100        /**
101         * Evaluate all the networks from the scan results.
102         *
103         * @param scanDetails    a list of scan details constructed from the scan results
104         * @param currentNetwork configuration of the current connected network
105         *                       or null if disconnected
106         * @param currentBssid   BSSID of the current connected network or null if
107         *                       disconnected
108         * @param connected      a flag to indicate if WifiStateMachine is in connected
109         *                       state
110         * @param untrustedNetworkAllowed a flag to indidate if untrusted networks like
111         *                                ephemeral networks are allowed
112         * @param connectableNetworks     a list of the ScanDetail and WifiConfiguration
113         *                                pair which is used by the WifiLastResortWatchdog
114         * @return configuration of the chosen network;
115         *         null if no network in this category is available.
116         */
117        @Nullable
118        WifiConfiguration evaluateNetworks(List<ScanDetail> scanDetails,
119                        WifiConfiguration currentNetwork, String currentBssid,
120                        boolean connected, boolean untrustedNetworkAllowed,
121                        List<Pair<ScanDetail, WifiConfiguration>> connectableNetworks);
122    }
123
124    private final NetworkEvaluator[] mEvaluators = new NetworkEvaluator[MAX_NUM_EVALUATORS];
125
126    // A helper to log debugging information in the local log buffer, which can
127    // be retrieved in bugreport.
128    private void localLog(String log) {
129        mLocalLog.log(log);
130    }
131
132    private boolean isCurrentNetworkSufficient(WifiInfo wifiInfo) {
133        WifiConfiguration network =
134                            mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
135
136        // Currently connected?
137        if (network == null) {
138            localLog("No current connected network.");
139            return false;
140        } else {
141            localLog("Current connected network: " + network.SSID
142                    + " , ID: " + network.networkId);
143        }
144
145        // Ephemeral network is not qualified.
146        if (network.ephemeral) {
147            localLog("Current network is an ephemeral one.");
148            return false;
149        }
150
151        // Open network is not qualified.
152        if (WifiConfigurationUtil.isConfigForOpenNetwork(network)) {
153            localLog("Current network is a open one.");
154            return false;
155        }
156
157        // 2.4GHz networks is not qualified.
158        if (wifiInfo.is24GHz()) {
159            localLog("Current network is 2.4GHz.");
160            return false;
161        }
162
163        // Is the current network's singnal strength qualified? It can only
164        // be a 5GHz network if we reach here.
165        int currentRssi = wifiInfo.getRssi();
166        if (wifiInfo.is5GHz() && currentRssi < mThresholdQualifiedRssi5) {
167            localLog("Current network band=" + (wifiInfo.is5GHz() ? "5GHz" : "2.4GHz")
168                    + ", RSSI[" + currentRssi + "]-acceptable but not qualified.");
169            return false;
170        }
171
172        return true;
173    }
174
175    private boolean isNetworkSelectionNeeded(List<ScanDetail> scanDetails, WifiInfo wifiInfo,
176                        boolean connected, boolean disconnected) {
177        if (scanDetails.size() == 0) {
178            localLog("Empty connectivity scan results. Skip network selection.");
179            return false;
180        }
181
182        if (connected) {
183            // Is roaming allowed?
184            if (!mEnableAutoJoinWhenAssociated) {
185                localLog("Switching networks in connected state is not allowed."
186                        + " Skip network selection.");
187                return false;
188            }
189
190            // Has it been at least the minimum interval since last network selection?
191            if (mLastNetworkSelectionTimeStamp != INVALID_TIME_STAMP) {
192                long gap = mClock.getElapsedSinceBootMillis()
193                            - mLastNetworkSelectionTimeStamp;
194                if (gap < MINIMUM_NETWORK_SELECTION_INTERVAL_MS) {
195                    localLog("Too short since last network selection: " + gap + " ms."
196                            + " Skip network selection.");
197                    return false;
198                }
199            }
200
201            if (isCurrentNetworkSufficient(wifiInfo)) {
202                localLog("Current connected network already sufficient. Skip network selection.");
203                return false;
204            } else {
205                localLog("Current connected network is not sufficient.");
206                return true;
207            }
208        } else if (disconnected) {
209            return true;
210        } else {
211            // No network selection if WifiStateMachine is in a state other than
212            // CONNECTED or DISCONNECTED.
213            localLog("WifiStateMachine is in neither CONNECTED nor DISCONNECTED state."
214                    + " Skip network selection.");
215            return false;
216        }
217    }
218
219    /**
220     * Format the given ScanResult as a scan ID for logging.
221     */
222    public static String toScanId(@Nullable ScanResult scanResult) {
223        return scanResult == null ? "NULL"
224                                  : String.format("%s:%s", scanResult.SSID, scanResult.BSSID);
225    }
226
227    /**
228     * Format the given WifiConfiguration as a SSID:netId string
229     */
230    public static String toNetworkString(WifiConfiguration network) {
231        if (network == null) {
232            return null;
233        }
234
235        return (network.SSID + ":" + network.networkId);
236    }
237
238    private List<ScanDetail> filterScanResults(List<ScanDetail> scanDetails,
239                HashSet<String> bssidBlacklist, boolean isConnected, String currentBssid) {
240        ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>();
241        List<ScanDetail> validScanDetails = new ArrayList<ScanDetail>();
242        StringBuffer noValidSsid = new StringBuffer();
243        StringBuffer blacklistedBssid = new StringBuffer();
244        StringBuffer lowRssi = new StringBuffer();
245        boolean scanResultsHaveCurrentBssid = false;
246
247        for (ScanDetail scanDetail : scanDetails) {
248            ScanResult scanResult = scanDetail.getScanResult();
249
250            if (TextUtils.isEmpty(scanResult.SSID)) {
251                noValidSsid.append(scanResult.BSSID).append(" / ");
252                continue;
253            }
254
255            // Check if the scan results contain the currently connected BSSID
256            if (scanResult.BSSID.equals(currentBssid)) {
257                scanResultsHaveCurrentBssid = true;
258            }
259
260            final String scanId = toScanId(scanResult);
261
262            if (bssidBlacklist.contains(scanResult.BSSID)) {
263                blacklistedBssid.append(scanId).append(" / ");
264                continue;
265            }
266
267            // Skip network with too weak signals.
268            if ((scanResult.is24GHz() && scanResult.level
269                    < mThresholdMinimumRssi24)
270                    || (scanResult.is5GHz() && scanResult.level
271                    < mThresholdMinimumRssi5)) {
272                lowRssi.append(scanId).append("(")
273                    .append(scanResult.is24GHz() ? "2.4GHz" : "5GHz")
274                    .append(")").append(scanResult.level).append(" / ");
275                continue;
276            }
277
278            validScanDetails.add(scanDetail);
279        }
280
281        // WNS listens to all single scan results. Some scan requests may not include
282        // the channel of the currently connected network, so the currently connected
283        // network won't show up in the scan results. We don't act on these scan results
284        // to avoid aggressive network switching which might trigger disconnection.
285        if (isConnected && !scanResultsHaveCurrentBssid) {
286            localLog("Current connected BSSID " + currentBssid + " is not in the scan results."
287                    + " Skip network selection.");
288            validScanDetails.clear();
289            return validScanDetails;
290        }
291
292        if (noValidSsid.length() != 0) {
293            localLog("Networks filtered out due to invalid SSID: " + noValidSsid);
294        }
295
296        if (blacklistedBssid.length() != 0) {
297            localLog("Networks filtered out due to blacklist: " + blacklistedBssid);
298        }
299
300        if (lowRssi.length() != 0) {
301            localLog("Networks filtered out due to low signal strength: " + lowRssi);
302        }
303
304        return validScanDetails;
305    }
306
307    /**
308     * @return the list of ScanDetails scored as potential candidates by the last run of
309     * selectNetwork, this will be empty if Network selector determined no selection was
310     * needed on last run. This includes scan details of sufficient signal strength, and
311     * had an associated WifiConfiguration.
312     */
313    public List<Pair<ScanDetail, WifiConfiguration>> getFilteredScanDetails() {
314        return mConnectableNetworks;
315    }
316
317    /**
318     * This API is called when user explicitly selects a network. Currently, it is used in following
319     * cases:
320     * (1) User explicitly chooses to connect to a saved network.
321     * (2) User saves a network after adding a new network.
322     * (3) User saves a network after modifying a saved network.
323     * Following actions will be triggered:
324     * 1. If this network is disabled, we need re-enable it again.
325     * 2. This network is favored over all the other networks visible in latest network
326     *    selection procedure.
327     *
328     * @param netId  ID for the network chosen by the user
329     * @return true -- There is change made to connection choice of any saved network.
330     *         false -- There is no change made to connection choice of any saved network.
331     */
332    public boolean setUserConnectChoice(int netId) {
333        localLog("userSelectNetwork: network ID=" + netId);
334        WifiConfiguration selected = mWifiConfigManager.getConfiguredNetwork(netId);
335
336        if (selected == null || selected.SSID == null) {
337            localLog("userSelectNetwork: Invalid configuration with nid=" + netId);
338            return false;
339        }
340
341        // Enable the network if it is disabled.
342        if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) {
343            mWifiConfigManager.updateNetworkSelectionStatus(netId,
344                    WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE);
345        }
346
347        boolean change = false;
348        String key = selected.configKey();
349        // This is only used for setting the connect choice timestamp for debugging purposes.
350        long currentTime = mClock.getWallClockMillis();
351        List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks();
352
353        for (WifiConfiguration network : savedNetworks) {
354            WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus();
355            if (network.networkId == selected.networkId) {
356                if (status.getConnectChoice() != null) {
357                    localLog("Remove user selection preference of " + status.getConnectChoice()
358                            + " Set Time: " + status.getConnectChoiceTimestamp() + " from "
359                            + network.SSID + " : " + network.networkId);
360                    mWifiConfigManager.clearNetworkConnectChoice(network.networkId);
361                    change = true;
362                }
363                continue;
364            }
365
366            if (status.getSeenInLastQualifiedNetworkSelection()
367                    && (status.getConnectChoice() == null
368                    || !status.getConnectChoice().equals(key))) {
369                localLog("Add key: " + key + " Set Time: " + currentTime + " to "
370                        + toNetworkString(network));
371                mWifiConfigManager.setNetworkConnectChoice(network.networkId, key, currentTime);
372                change = true;
373            }
374        }
375
376        return change;
377    }
378
379    /**
380     * Overrides the {@code candidate} chosen by the {@link #mEvaluators} with the user chosen
381     * {@link WifiConfiguration} if one exists.
382     *
383     * @return the user chosen {@link WifiConfiguration} if one exists, {@code candidate} otherwise
384     */
385    private WifiConfiguration overrideCandidateWithUserConnectChoice(
386            @NonNull WifiConfiguration candidate) {
387        WifiConfiguration tempConfig = candidate;
388        ScanResult scanResultCandidate = candidate.getNetworkSelectionStatus().getCandidate();
389
390        while (tempConfig.getNetworkSelectionStatus().getConnectChoice() != null) {
391            String key = tempConfig.getNetworkSelectionStatus().getConnectChoice();
392            tempConfig = mWifiConfigManager.getConfiguredNetwork(key);
393
394            if (tempConfig != null) {
395                WifiConfiguration.NetworkSelectionStatus tempStatus =
396                        tempConfig.getNetworkSelectionStatus();
397                if (tempStatus.getCandidate() != null && tempStatus.isNetworkEnabled()) {
398                    scanResultCandidate = tempStatus.getCandidate();
399                    candidate = tempConfig;
400                }
401            } else {
402                localLog("Connect choice: " + key + " has no corresponding saved config.");
403                break;
404            }
405        }
406        localLog("After user selection adjustment, the final candidate is:"
407                + WifiNetworkSelector.toNetworkString(candidate) + " : "
408                + scanResultCandidate.BSSID);
409        return candidate;
410    }
411
412    /**
413     * Select the best network from the ones in range.
414     *
415     * @param scanDetails    List of ScanDetail for all the APs in range
416     * @param bssidBlacklist Blacklisted BSSIDs
417     * @param wifiInfo       Currently connected network
418     * @param connected      True if the device is connected
419     * @param disconnected   True if the device is disconnected
420     * @param untrustedNetworkAllowed True if untrusted networks are allowed for connection
421     * @return Configuration of the selected network, or Null if nothing
422     */
423    @Nullable
424    public WifiConfiguration selectNetwork(List<ScanDetail> scanDetails,
425            HashSet<String> bssidBlacklist, WifiInfo wifiInfo,
426            boolean connected, boolean disconnected, boolean untrustedNetworkAllowed) {
427        mConnectableNetworks.clear();
428        if (scanDetails.size() == 0) {
429            localLog("Empty connectivity scan result");
430            return null;
431        }
432
433        WifiConfiguration currentNetwork =
434                mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
435
436        // Always get the current BSSID from WifiInfo in case that firmware initiated
437        // roaming happened.
438        String currentBssid = wifiInfo.getBSSID();
439
440        // Shall we start network selection at all?
441        if (!isNetworkSelectionNeeded(scanDetails, wifiInfo, connected, disconnected)) {
442            return null;
443        }
444
445        // Update the registered network evaluators.
446        for (NetworkEvaluator registeredEvaluator : mEvaluators) {
447            if (registeredEvaluator != null) {
448                registeredEvaluator.update(scanDetails);
449            }
450        }
451
452        // Filter out unwanted networks.
453        List<ScanDetail> filteredScanDetails = filterScanResults(scanDetails, bssidBlacklist,
454                connected, currentBssid);
455        if (filteredScanDetails.size() == 0) {
456            return null;
457        }
458
459        // Go through the registered network evaluators from the highest priority
460        // one to the lowest till a network is selected.
461        WifiConfiguration selectedNetwork = null;
462        for (NetworkEvaluator registeredEvaluator : mEvaluators) {
463            if (registeredEvaluator != null) {
464                selectedNetwork = registeredEvaluator.evaluateNetworks(filteredScanDetails,
465                            currentNetwork, currentBssid, connected,
466                            untrustedNetworkAllowed, mConnectableNetworks);
467                if (selectedNetwork != null) {
468                    break;
469                }
470            }
471        }
472
473        if (selectedNetwork != null) {
474            selectedNetwork = overrideCandidateWithUserConnectChoice(selectedNetwork);
475            mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis();
476        }
477
478        return selectedNetwork;
479    }
480
481    /**
482     * Register a network evaluator
483     *
484     * @param evaluator the network evaluator to be registered
485     * @param priority a value between 0 and (SCORER_MIN_PRIORITY-1)
486     *
487     * @return true if the evaluator is successfully registered with QNS;
488     *         false if failed to register the evaluator
489     */
490    public boolean registerNetworkEvaluator(NetworkEvaluator evaluator, int priority) {
491        if (priority < 0 || priority >= EVALUATOR_MIN_PRIORITY) {
492            localLog("Invalid network evaluator priority: " + priority);
493            return false;
494        }
495
496        if (mEvaluators[priority] != null) {
497            localLog("Priority " + priority + " is already registered by "
498                    + mEvaluators[priority].getName());
499            return false;
500        }
501
502        mEvaluators[priority] = evaluator;
503        return true;
504    }
505
506    WifiNetworkSelector(Context context, WifiConfigManager configManager, Clock clock,
507            LocalLog localLog) {
508        mWifiConfigManager = configManager;
509        mClock = clock;
510        mLocalLog = localLog;
511
512        mThresholdQualifiedRssi24 = context.getResources().getInteger(
513                            R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_24GHz);
514        mThresholdQualifiedRssi5 = context.getResources().getInteger(
515                            R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_5GHz);
516        mThresholdMinimumRssi24 = context.getResources().getInteger(
517                            R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_24GHz);
518        mThresholdMinimumRssi5 = context.getResources().getInteger(
519                            R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_5GHz);
520        mEnableAutoJoinWhenAssociated = context.getResources().getBoolean(
521                            R.bool.config_wifi_framework_enable_associated_network_selection);
522    }
523}
524