blob: 956fe629c89046a4487f38738e1b3390fef69d9c [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.wifi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.Context;
import android.net.NetworkKey;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.text.TextUtils;
import android.util.LocalLog;
import android.util.Pair;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* This class looks at all the connectivity scan results then
* selects a network for the phone to connect or roam to.
*/
public class WifiNetworkSelector {
private static final long INVALID_TIME_STAMP = Long.MIN_VALUE;
// Minimum time gap between last successful network selection and a new selection
// attempt.
@VisibleForTesting
public static final int MINIMUM_NETWORK_SELECTION_INTERVAL_MS = 10 * 1000;
// Constants for BSSID blacklist.
public static final int BSSID_BLACKLIST_THRESHOLD = 3;
public static final int BSSID_BLACKLIST_EXPIRE_TIME_MS = 5 * 60 * 1000;
// Association success/failure reason codes
@VisibleForTesting
public static final int REASON_CODE_AP_UNABLE_TO_HANDLE_NEW_STA = 17;
private WifiConfigManager mWifiConfigManager;
private Clock mClock;
private static class BssidBlacklistStatus {
// Number of times this BSSID has been rejected for association.
public int counter;
public boolean isBlacklisted;
public long blacklistedTimeStamp = INVALID_TIME_STAMP;
}
private Map<String, BssidBlacklistStatus> mBssidBlacklist =
new HashMap<>();
private final LocalLog mLocalLog =
new LocalLog(ActivityManager.isLowRamDeviceStatic() ? 256 : 512);
private long mLastNetworkSelectionTimeStamp = INVALID_TIME_STAMP;
// Buffer of filtered scan results (Scan results considered by network selection) & associated
// WifiConfiguration (if any).
private volatile List<Pair<ScanDetail, WifiConfiguration>> mConnectableNetworks =
new ArrayList<>();
private final int mThresholdQualifiedRssi24;
private final int mThresholdQualifiedRssi5;
private final int mThresholdMinimumRssi24;
private final int mThresholdMinimumRssi5;
private final boolean mEnableAutoJoinWhenAssociated;
/**
* WiFi Network Selector supports various types of networks. Each type can
* have its evaluator to choose the best WiFi network for the device to connect
* to. When registering a WiFi network evaluator with the WiFi Network Selector,
* the priority of the network must be specified, and it must be a value between
* 0 and (EVALUATOR_MIN_PIRORITY - 1) with 0 being the highest priority. Wifi
* Network Selector iterates through the registered scorers from the highest priority
* to the lowest till a network is selected.
*/
public static final int EVALUATOR_MIN_PRIORITY = 6;
/**
* Maximum number of evaluators can be registered with Wifi Network Selector.
*/
public static final int MAX_NUM_EVALUATORS = EVALUATOR_MIN_PRIORITY;
/**
* Interface for WiFi Network Evaluator
*
* A network scorer evaulates all the networks from the scan results and
* recommends the best network in its category to connect or roam to.
*/
public interface NetworkEvaluator {
/**
* Get the evaluator name.
*/
String getName();
/**
* Update the evaluator.
*
* Certain evaluators have to be updated with the new scan results. For example
* the ExternalScoreEvalutor needs to refresh its Score Cache.
*
* @param scanDetails a list of scan details constructed from the scan results
*/
void update(List<ScanDetail> scanDetails);
/**
* Evaluate all the networks from the scan results.
*
* @param scanDetails a list of scan details constructed from the scan results
* @param currentNetwork configuration of the current connected network
* or null if disconnected
* @param currentBssid BSSID of the current connected network or null if
* disconnected
* @param connected a flag to indicate if WifiStateMachine is in connected
* state
* @param untrustedNetworkAllowed a flag to indidate if untrusted networks like
* ephemeral networks are allowed
* @param connectableNetworks a list of the ScanDetail and WifiConfiguration
* pair which is used by the WifiLastResortWatchdog
* @return configuration of the chosen network;
* null if no network in this category is available.
*/
@Nullable
WifiConfiguration evaluateNetworks(List<ScanDetail> scanDetails,
WifiConfiguration currentNetwork, String currentBssid,
boolean connected, boolean untrustedNetworkAllowed,
List<Pair<ScanDetail, WifiConfiguration>> connectableNetworks);
}
private final NetworkEvaluator[] mEvaluators = new NetworkEvaluator[MAX_NUM_EVALUATORS];
// A helper to log debugging information in the local log buffer, which can
// be retrieved in bugreport.
private void localLog(String log) {
mLocalLog.log(log);
}
private boolean isCurrentNetworkSufficient(WifiInfo wifiInfo) {
WifiConfiguration network =
mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
// Currently connected?
if (network == null) {
localLog("No current connected network.");
return false;
} else {
localLog("Current connected network: " + network.SSID
+ " , ID: " + network.networkId);
}
// Ephemeral network is not qualified.
if (network.ephemeral) {
localLog("Current network is an ephemeral one.");
return false;
}
// Open network is not qualified.
if (WifiConfigurationUtil.isConfigForOpenNetwork(network)) {
localLog("Current network is a open one.");
return false;
}
// 2.4GHz networks is not qualified.
if (wifiInfo.is24GHz()) {
localLog("Current network is 2.4GHz.");
return false;
}
// Is the current network's singnal strength qualified? It can only
// be a 5GHz network if we reach here.
int currentRssi = wifiInfo.getRssi();
if (wifiInfo.is5GHz() && currentRssi < mThresholdQualifiedRssi5) {
localLog("Current network band=" + (wifiInfo.is5GHz() ? "5GHz" : "2.4GHz")
+ ", RSSI[" + currentRssi + "]-acceptable but not qualified.");
return false;
}
return true;
}
private boolean isNetworkSelectionNeeded(List<ScanDetail> scanDetails, WifiInfo wifiInfo,
boolean connected, boolean disconnected) {
if (scanDetails.size() == 0) {
localLog("Empty connectivity scan results. Skip network selection.");
return false;
}
if (connected) {
// Is roaming allowed?
if (!mEnableAutoJoinWhenAssociated) {
localLog("Switching networks in connected state is not allowed."
+ " Skip network selection.");
return false;
}
// Has it been at least the minimum interval since last network selection?
if (mLastNetworkSelectionTimeStamp != INVALID_TIME_STAMP) {
long gap = mClock.getElapsedSinceBootMillis()
- mLastNetworkSelectionTimeStamp;
if (gap < MINIMUM_NETWORK_SELECTION_INTERVAL_MS) {
localLog("Too short since last network selection: " + gap + " ms."
+ " Skip network selection.");
return false;
}
}
if (isCurrentNetworkSufficient(wifiInfo)) {
localLog("Current connected network already sufficient. Skip network selection.");
return false;
} else {
localLog("Current connected network is not sufficient.");
return true;
}
} else if (disconnected) {
return true;
} else {
// No network selection if WifiStateMachine is in a state other than
// CONNECTED or DISCONNECTED.
localLog("WifiStateMachine is in neither CONNECTED nor DISCONNECTED state."
+ " Skip network selection.");
return false;
}
}
/**
* Format the given ScanResult as a scan ID for logging.
*/
public static String toScanId(@Nullable ScanResult scanResult) {
return scanResult == null ? "NULL"
: String.format("%s:%s", scanResult.SSID, scanResult.BSSID);
}
/**
* Format the given WifiConfiguration as a SSID:netId string
*/
public static String toNetworkString(WifiConfiguration network) {
if (network == null) {
return null;
}
return (network.SSID + ":" + network.networkId);
}
private List<ScanDetail> filterScanResults(List<ScanDetail> scanDetails, boolean isConnected,
String currentBssid) {
ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>();
List<ScanDetail> validScanDetails = new ArrayList<ScanDetail>();
StringBuffer noValidSsid = new StringBuffer();
StringBuffer blacklistedBssid = new StringBuffer();
StringBuffer lowRssi = new StringBuffer();
boolean scanResultsHaveCurrentBssid = false;
for (ScanDetail scanDetail : scanDetails) {
ScanResult scanResult = scanDetail.getScanResult();
if (TextUtils.isEmpty(scanResult.SSID)) {
noValidSsid.append(scanResult.BSSID).append(" / ");
continue;
}
// Check if the scan results contain the currently connected BSSID
if (scanResult.BSSID.equals(currentBssid)) {
scanResultsHaveCurrentBssid = true;
}
final String scanId = toScanId(scanResult);
if (isBssidDisabled(scanResult.BSSID)) {
blacklistedBssid.append(scanId).append(" / ");
continue;
}
// Skip network with too weak signals.
if ((scanResult.is24GHz() && scanResult.level
< mThresholdMinimumRssi24)
|| (scanResult.is5GHz() && scanResult.level
< mThresholdMinimumRssi5)) {
lowRssi.append(scanId).append("(")
.append(scanResult.is24GHz() ? "2.4GHz" : "5GHz")
.append(")").append(scanResult.level).append(" / ");
continue;
}
validScanDetails.add(scanDetail);
}
// WNS listens to all single scan results. Some scan requests may not include
// the channel of the currently connected network, so the currently connected
// network won't show up in the scan results. We don't act on these scan results
// to avoid aggressive network switching which might trigger disconnection.
if (isConnected && !scanResultsHaveCurrentBssid) {
localLog("Current connected BSSID " + currentBssid + " is not in the scan results."
+ " Skip network selection.");
validScanDetails.clear();
return validScanDetails;
}
if (noValidSsid.length() != 0) {
localLog("Networks filtered out due to invalid SSID: " + noValidSsid);
}
if (blacklistedBssid.length() != 0) {
localLog("Networks filtered out due to blacklist: " + blacklistedBssid);
}
if (lowRssi.length() != 0) {
localLog("Networks filtered out due to low signal strength: " + lowRssi);
}
return validScanDetails;
}
/**
* @return the list of ScanDetails scored as potential candidates by the last run of
* selectNetwork, this will be empty if Network selector determined no selection was
* needed on last run. This includes scan details of sufficient signal strength, and
* had an associated WifiConfiguration.
*/
public List<Pair<ScanDetail, WifiConfiguration>> getFilteredScanDetails() {
return mConnectableNetworks;
}
/**
* This API is called when user explicitly selects a network. Currently, it is used in following
* cases:
* (1) User explicitly chooses to connect to a saved network.
* (2) User saves a network after adding a new network.
* (3) User saves a network after modifying a saved network.
* Following actions will be triggered:
* 1. If this network is disabled, we need re-enable it again.
* 2. This network is favored over all the other networks visible in latest network
* selection procedure.
*
* @param netId ID for the network chosen by the user
* @return true -- There is change made to connection choice of any saved network.
* false -- There is no change made to connection choice of any saved network.
*/
public boolean setUserConnectChoice(int netId) {
localLog("userSelectNetwork: network ID=" + netId);
WifiConfiguration selected = mWifiConfigManager.getConfiguredNetwork(netId);
if (selected == null || selected.SSID == null) {
localLog("userSelectNetwork: Invalid configuration with nid=" + netId);
return false;
}
// Enable the network if it is disabled.
if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) {
mWifiConfigManager.updateNetworkSelectionStatus(netId,
WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE);
}
boolean change = false;
String key = selected.configKey();
// This is only used for setting the connect choice timestamp for debugging purposes.
long currentTime = mClock.getWallClockMillis();
List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks();
for (WifiConfiguration network : savedNetworks) {
WifiConfiguration.NetworkSelectionStatus status = network.getNetworkSelectionStatus();
if (network.networkId == selected.networkId) {
if (status.getConnectChoice() != null) {
localLog("Remove user selection preference of " + status.getConnectChoice()
+ " Set Time: " + status.getConnectChoiceTimestamp() + " from "
+ network.SSID + " : " + network.networkId);
mWifiConfigManager.clearNetworkConnectChoice(network.networkId);
change = true;
}
continue;
}
if (status.getSeenInLastQualifiedNetworkSelection()
&& (status.getConnectChoice() == null
|| !status.getConnectChoice().equals(key))) {
localLog("Add key: " + key + " Set Time: " + currentTime + " to "
+ toNetworkString(network));
mWifiConfigManager.setNetworkConnectChoice(network.networkId, key, currentTime);
change = true;
}
}
return change;
}
/**
* Overrides the {@code candidate} chosen by the {@link #mEvaluators} with the user chosen
* {@link WifiConfiguration} if one exists.
*
* @return the user chosen {@link WifiConfiguration} if one exists, {@code candidate} otherwise
*/
private WifiConfiguration overrideCandidateWithUserConnectChoice(
@NonNull WifiConfiguration candidate) {
WifiConfiguration tempConfig = candidate;
ScanResult scanResultCandidate = candidate.getNetworkSelectionStatus().getCandidate();
while (tempConfig.getNetworkSelectionStatus().getConnectChoice() != null) {
String key = tempConfig.getNetworkSelectionStatus().getConnectChoice();
tempConfig = mWifiConfigManager.getConfiguredNetwork(key);
if (tempConfig != null) {
WifiConfiguration.NetworkSelectionStatus tempStatus =
tempConfig.getNetworkSelectionStatus();
if (tempStatus.getCandidate() != null && tempStatus.isNetworkEnabled()) {
scanResultCandidate = tempStatus.getCandidate();
candidate = tempConfig;
}
} else {
localLog("Connect choice: " + key + " has no corresponding saved config.");
break;
}
}
localLog("After user selection adjustment, the final candidate is:"
+ WifiNetworkSelector.toNetworkString(candidate) + " : "
+ scanResultCandidate.BSSID);
return candidate;
}
/**
* Enable/disable a BSSID for Network Selection
* When an association rejection event is obtained, Network Selector will disable this
* BSSID but supplicant still can try to connect to this bssid. If supplicant connect to it
* successfully later, this bssid can be re-enabled.
*
* @param bssid the bssid to be enabled / disabled
* @param enable -- true enable a bssid if it has been disabled
* -- false disable a bssid
* @param reasonCode enable/disable reason code
*/
public boolean enableBssidForNetworkSelection(String bssid, boolean enable, int reasonCode) {
if (enable) {
return (mBssidBlacklist.remove(bssid) != null);
} else {
if (bssid != null) {
BssidBlacklistStatus status = mBssidBlacklist.get(bssid);
if (status == null) {
// First time for this BSSID
status = new BssidBlacklistStatus();
mBssidBlacklist.put(bssid, status);
}
if (!status.isBlacklisted) {
status.counter++;
if (status.counter >= BSSID_BLACKLIST_THRESHOLD
|| reasonCode == REASON_CODE_AP_UNABLE_TO_HANDLE_NEW_STA) {
status.isBlacklisted = true;
status.blacklistedTimeStamp = mClock.getElapsedSinceBootMillis();
return true;
}
}
}
}
return false;
}
/**
* Update the BSSID blacklist
*
* Go through the BSSID blacklist and check when a BSSID was blocked. If it
* has been blacklisted for BSSID_BLACKLIST_EXPIRE_TIME_MS, then re-enable it.
*/
private void updateBssidBlacklist() {
Iterator<BssidBlacklistStatus> iter = mBssidBlacklist.values().iterator();
while (iter.hasNext()) {
BssidBlacklistStatus status = iter.next();
if (status != null && status.isBlacklisted) {
if (mClock.getElapsedSinceBootMillis() - status.blacklistedTimeStamp
>= BSSID_BLACKLIST_EXPIRE_TIME_MS) {
iter.remove();
}
}
}
}
/**
* Check whether a bssid is disabled
* @param bssid -- the bssid to check
*/
private boolean isBssidDisabled(String bssid) {
BssidBlacklistStatus status = mBssidBlacklist.get(bssid);
return status == null ? false : status.isBlacklisted;
}
/**
*
*/
@Nullable
public WifiConfiguration selectNetwork(List<ScanDetail> scanDetails, WifiInfo wifiInfo,
boolean connected, boolean disconnected, boolean untrustedNetworkAllowed) {
mConnectableNetworks.clear();
if (scanDetails.size() == 0) {
localLog("Empty connectivity scan result");
return null;
}
WifiConfiguration currentNetwork =
mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
// Always get the current BSSID from WifiInfo in case that firmware initiated
// roaming happened.
String currentBssid = wifiInfo.getBSSID();
// Shall we start network selection at all?
if (!isNetworkSelectionNeeded(scanDetails, wifiInfo, connected, disconnected)) {
return null;
}
// Update the registered network evaluators.
for (NetworkEvaluator registeredEvaluator : mEvaluators) {
if (registeredEvaluator != null) {
registeredEvaluator.update(scanDetails);
}
}
// Check if any BSSID can be freed from the blacklist.
updateBssidBlacklist();
// Filter out unwanted networks.
List<ScanDetail> filteredScanDetails = filterScanResults(scanDetails, connected,
currentBssid);
if (filteredScanDetails.size() == 0) {
return null;
}
// Go through the registered network evaluators from the highest priority
// one to the lowest till a network is selected.
WifiConfiguration selectedNetwork = null;
for (NetworkEvaluator registeredEvaluator : mEvaluators) {
if (registeredEvaluator != null) {
selectedNetwork = registeredEvaluator.evaluateNetworks(filteredScanDetails,
currentNetwork, currentBssid, connected,
untrustedNetworkAllowed, mConnectableNetworks);
if (selectedNetwork != null) {
break;
}
}
}
if (selectedNetwork != null) {
selectedNetwork = overrideCandidateWithUserConnectChoice(selectedNetwork);
mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis();
}
return selectedNetwork;
}
/**
* Register a network evaluator
*
* @param evaluator the network evaluator to be registered
* @param priority a value between 0 and (SCORER_MIN_PRIORITY-1)
*
* @return true if the evaluator is successfully registered with QNS;
* false if failed to register the evaluator
*/
public boolean registerNetworkEvaluator(NetworkEvaluator evaluator, int priority) {
if (priority < 0 || priority >= EVALUATOR_MIN_PRIORITY) {
localLog("Invalid network evaluator priority: " + priority);
return false;
}
if (mEvaluators[priority] != null) {
localLog("Priority " + priority + " is already registered by "
+ mEvaluators[priority].getName());
return false;
}
mEvaluators[priority] = evaluator;
return true;
}
WifiNetworkSelector(Context context, WifiConfigManager configManager, Clock clock) {
mWifiConfigManager = configManager;
mClock = clock;
mThresholdQualifiedRssi24 = context.getResources().getInteger(
R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_24GHz);
mThresholdQualifiedRssi5 = context.getResources().getInteger(
R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_5GHz);
mThresholdMinimumRssi24 = context.getResources().getInteger(
R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_24GHz);
mThresholdMinimumRssi5 = context.getResources().getInteger(
R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_5GHz);
mEnableAutoJoinWhenAssociated = context.getResources().getBoolean(
R.bool.config_wifi_framework_enable_associated_network_selection);
}
/**
* Retrieve the local log buffer created by WifiNetworkSelector.
*/
public LocalLog getLocalLog() {
return mLocalLog;
}
/**
* Dump the local logs.
*/
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("Dump of WifiNetworkSelector");
pw.println("WifiNetworkSelector - Log Begin ----");
mLocalLog.dump(fd, pw, args);
pw.println("WifiNetworkSelector - Log End ----");
}
}