Home | History | Annotate | Download | only in wifi
      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 
     17 package com.android.server.wifi;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.content.Context;
     22 import android.net.NetworkKey;
     23 import android.net.wifi.ScanResult;
     24 import android.net.wifi.WifiConfiguration;
     25 import android.net.wifi.WifiInfo;
     26 import android.text.TextUtils;
     27 import android.util.LocalLog;
     28 import android.util.Pair;
     29 
     30 import com.android.internal.R;
     31 import com.android.internal.annotations.VisibleForTesting;
     32 
     33 import java.util.ArrayList;
     34 import java.util.HashSet;
     35 import 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  */
     41 public 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         WifiConfiguration originalCandidate = candidate;
    389         ScanResult scanResultCandidate = candidate.getNetworkSelectionStatus().getCandidate();
    390 
    391         while (tempConfig.getNetworkSelectionStatus().getConnectChoice() != null) {
    392             String key = tempConfig.getNetworkSelectionStatus().getConnectChoice();
    393             tempConfig = mWifiConfigManager.getConfiguredNetwork(key);
    394 
    395             if (tempConfig != null) {
    396                 WifiConfiguration.NetworkSelectionStatus tempStatus =
    397                         tempConfig.getNetworkSelectionStatus();
    398                 if (tempStatus.getCandidate() != null && tempStatus.isNetworkEnabled()) {
    399                     scanResultCandidate = tempStatus.getCandidate();
    400                     candidate = tempConfig;
    401                 }
    402             } else {
    403                 localLog("Connect choice: " + key + " has no corresponding saved config.");
    404                 break;
    405             }
    406         }
    407 
    408         if (candidate != originalCandidate) {
    409             localLog("After user selection adjustment, the final candidate is:"
    410                     + WifiNetworkSelector.toNetworkString(candidate) + " : "
    411                     + scanResultCandidate.BSSID);
    412         }
    413         return candidate;
    414     }
    415 
    416     /**
    417      * Select the best network from the ones in range.
    418      *
    419      * @param scanDetails    List of ScanDetail for all the APs in range
    420      * @param bssidBlacklist Blacklisted BSSIDs
    421      * @param wifiInfo       Currently connected network
    422      * @param connected      True if the device is connected
    423      * @param disconnected   True if the device is disconnected
    424      * @param untrustedNetworkAllowed True if untrusted networks are allowed for connection
    425      * @return Configuration of the selected network, or Null if nothing
    426      */
    427     @Nullable
    428     public WifiConfiguration selectNetwork(List<ScanDetail> scanDetails,
    429             HashSet<String> bssidBlacklist, WifiInfo wifiInfo,
    430             boolean connected, boolean disconnected, boolean untrustedNetworkAllowed) {
    431         mConnectableNetworks.clear();
    432         if (scanDetails.size() == 0) {
    433             localLog("Empty connectivity scan result");
    434             return null;
    435         }
    436 
    437         WifiConfiguration currentNetwork =
    438                 mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
    439 
    440         // Always get the current BSSID from WifiInfo in case that firmware initiated
    441         // roaming happened.
    442         String currentBssid = wifiInfo.getBSSID();
    443 
    444         // Shall we start network selection at all?
    445         if (!isNetworkSelectionNeeded(scanDetails, wifiInfo, connected, disconnected)) {
    446             return null;
    447         }
    448 
    449         // Update the registered network evaluators.
    450         for (NetworkEvaluator registeredEvaluator : mEvaluators) {
    451             if (registeredEvaluator != null) {
    452                 registeredEvaluator.update(scanDetails);
    453             }
    454         }
    455 
    456         // Filter out unwanted networks.
    457         List<ScanDetail> filteredScanDetails = filterScanResults(scanDetails, bssidBlacklist,
    458                 connected, currentBssid);
    459         if (filteredScanDetails.size() == 0) {
    460             return null;
    461         }
    462 
    463         // Go through the registered network evaluators from the highest priority
    464         // one to the lowest till a network is selected.
    465         WifiConfiguration selectedNetwork = null;
    466         for (NetworkEvaluator registeredEvaluator : mEvaluators) {
    467             if (registeredEvaluator != null) {
    468                 localLog("About to run " + registeredEvaluator.getName() + " :");
    469                 selectedNetwork = registeredEvaluator.evaluateNetworks(filteredScanDetails,
    470                             currentNetwork, currentBssid, connected,
    471                             untrustedNetworkAllowed, mConnectableNetworks);
    472                 if (selectedNetwork != null) {
    473                     localLog(registeredEvaluator.getName() + " selects "
    474                             + WifiNetworkSelector.toNetworkString(selectedNetwork) + " : "
    475                             + selectedNetwork.getNetworkSelectionStatus().getCandidate().BSSID);
    476                     break;
    477                 }
    478             }
    479         }
    480 
    481         if (selectedNetwork != null) {
    482             selectedNetwork = overrideCandidateWithUserConnectChoice(selectedNetwork);
    483             mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis();
    484         }
    485 
    486         return selectedNetwork;
    487     }
    488 
    489     /**
    490      * Register a network evaluator
    491      *
    492      * @param evaluator the network evaluator to be registered
    493      * @param priority a value between 0 and (SCORER_MIN_PRIORITY-1)
    494      *
    495      * @return true if the evaluator is successfully registered with QNS;
    496      *         false if failed to register the evaluator
    497      */
    498     public boolean registerNetworkEvaluator(NetworkEvaluator evaluator, int priority) {
    499         if (priority < 0 || priority >= EVALUATOR_MIN_PRIORITY) {
    500             localLog("Invalid network evaluator priority: " + priority);
    501             return false;
    502         }
    503 
    504         if (mEvaluators[priority] != null) {
    505             localLog("Priority " + priority + " is already registered by "
    506                     + mEvaluators[priority].getName());
    507             return false;
    508         }
    509 
    510         mEvaluators[priority] = evaluator;
    511         return true;
    512     }
    513 
    514     WifiNetworkSelector(Context context, WifiConfigManager configManager, Clock clock,
    515             LocalLog localLog) {
    516         mWifiConfigManager = configManager;
    517         mClock = clock;
    518         mLocalLog = localLog;
    519 
    520         mThresholdQualifiedRssi24 = context.getResources().getInteger(
    521                             R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_24GHz);
    522         mThresholdQualifiedRssi5 = context.getResources().getInteger(
    523                             R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_5GHz);
    524         mThresholdMinimumRssi24 = context.getResources().getInteger(
    525                             R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_24GHz);
    526         mThresholdMinimumRssi5 = context.getResources().getInteger(
    527                             R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_5GHz);
    528         mEnableAutoJoinWhenAssociated = context.getResources().getBoolean(
    529                             R.bool.config_wifi_framework_enable_associated_network_selection);
    530     }
    531 }
    532