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