blob: a1a9a821329ef94b7d0e3bf7446a29b6b370dadf [file] [log] [blame]
/*
* Copyright (C) 2014 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.content.Context;
import android.net.NetworkKey;
import android.net.NetworkScoreManager;
import android.net.WifiKey;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiConfiguration.KeyMgmt;
import android.net.wifi.WifiConnectionStatistics;
import android.os.Process;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.os.SystemClock;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
/**
* AutoJoin controller is responsible for WiFi Connect decision
*
* It runs in the thread context of WifiStateMachine
*
*/
public class WifiAutoJoinController {
private Context mContext;
private WifiStateMachine mWifiStateMachine;
private WifiConfigStore mWifiConfigStore;
private WifiNative mWifiNative;
private NetworkScoreManager scoreManager;
private WifiNetworkScoreCache mNetworkScoreCache;
private static final String TAG = "WifiAutoJoinController ";
private static boolean DBG = false;
private static boolean VDBG = false;
private static final boolean mStaStaSupported = false;
public static int mScanResultMaximumAge = 40000; /* milliseconds unit */
public static int mScanResultAutoJoinAge = 5000; /* milliseconds unit */
private String mCurrentConfigurationKey = null; //used by autojoin
private final HashMap<String, ScanDetail> scanResultCache = new HashMap<>();
private WifiConnectionStatistics mWifiConnectionStatistics;
/**
* Whether to allow connections to untrusted networks.
*/
private boolean mAllowUntrustedConnections = false;
/* For debug purpose only: if the scored override a score */
boolean didOverride = false;
// Lose the non-auth failure blacklisting after 8 hours
private final static long loseBlackListHardMilli = 1000 * 60 * 60 * 8;
// Lose some temporary blacklisting after 30 minutes
private final static long loseBlackListSoftMilli = 1000 * 60 * 30;
/**
* @see android.provider.Settings.Global#WIFI_EPHEMERAL_OUT_OF_RANGE_TIMEOUT_MS
*/
private static final long DEFAULT_EPHEMERAL_OUT_OF_RANGE_TIMEOUT_MS = 1000 * 60; // 1 minute
public static final int AUTO_JOIN_IDLE = 0;
public static final int AUTO_JOIN_ROAMING = 1;
public static final int AUTO_JOIN_EXTENDED_ROAMING = 2;
public static final int AUTO_JOIN_OUT_OF_NETWORK_ROAMING = 3;
public static final int HIGH_THRESHOLD_MODIFIER = 5;
public static final int MAX_RSSI_DELTA = 50;
// Below are AutoJoin wide parameters indicating if we should be aggressive before joining
// weak network. Note that we cannot join weak network that are going to be marked as unanted by
// ConnectivityService because this will trigger link flapping.
/**
* There was a non-blacklisted configuration that we bailed from because of a weak signal
*/
boolean didBailDueToWeakRssi = false;
/**
* number of time we consecutively bailed out of an eligible network because its signal
* was too weak
*/
int weakRssiBailCount = 0;
WifiAutoJoinController(Context c, WifiStateMachine w, WifiConfigStore s,
WifiConnectionStatistics st, WifiNative n) {
mContext = c;
mWifiStateMachine = w;
mWifiConfigStore = s;
mWifiNative = n;
mNetworkScoreCache = null;
mWifiConnectionStatistics = st;
scoreManager =
(NetworkScoreManager) mContext.getSystemService(Context.NETWORK_SCORE_SERVICE);
if (scoreManager == null)
logDbg("Registered scoreManager NULL " + " service " + Context.NETWORK_SCORE_SERVICE);
if (scoreManager != null) {
mNetworkScoreCache = new WifiNetworkScoreCache(mContext);
scoreManager.registerNetworkScoreCache(NetworkKey.TYPE_WIFI, mNetworkScoreCache);
} else {
logDbg("No network score service: Couldnt register as a WiFi score Manager, type="
+ Integer.toString(NetworkKey.TYPE_WIFI)
+ " service " + Context.NETWORK_SCORE_SERVICE);
mNetworkScoreCache = null;
}
}
void enableVerboseLogging(int verbose) {
if (verbose > 0) {
DBG = true;
VDBG = true;
} else {
DBG = false;
VDBG = false;
}
}
/**
* Flush out scan results older than mScanResultMaximumAge
*/
private void ageScanResultsOut(int delay) {
if (delay <= 0) {
delay = mScanResultMaximumAge; // Something sane
}
long milli = System.currentTimeMillis();
if (VDBG) {
logDbg("ageScanResultsOut delay " + Integer.valueOf(delay) + " size "
+ Integer.valueOf(scanResultCache.size()) + " now " + Long.valueOf(milli));
}
Iterator<HashMap.Entry<String, ScanDetail>> iter = scanResultCache.entrySet().iterator();
while (iter.hasNext()) {
HashMap.Entry<String, ScanDetail> entry = iter.next();
ScanDetail scanDetail = entry.getValue();
if ((scanDetail.getSeen() + delay) < milli) {
iter.remove();
}
}
}
void averageRssiAndRemoveFromCache(ScanResult result) {
// Fetch the previous instance for this result
ScanDetail sd = scanResultCache.get(result.BSSID);
if (sd != null) {
ScanResult sr = sd.getScanResult();
if (mWifiConfigStore.scanResultRssiLevelPatchUp != 0
&& result.level == 0
&& sr.level < -20) {
// A 'zero' RSSI reading is most likely a chip problem which returns
// an unknown RSSI, hence ignore it
result.level = sr.level;
}
// If there was a previous cache result for this BSSID, average the RSSI values
result.averageRssi(sr.level, sr.seen, mScanResultMaximumAge);
// Remove the previous Scan Result - this is not necessary
scanResultCache.remove(result.BSSID);
} else if (mWifiConfigStore.scanResultRssiLevelPatchUp != 0 && result.level == 0) {
// A 'zero' RSSI reading is most likely a chip problem which returns
// an unknown RSSI, hence initialize it to a sane value
result.level = mWifiConfigStore.scanResultRssiLevelPatchUp;
}
}
void addToUnscoredNetworks(ScanResult result, List<NetworkKey> unknownScanResults) {
WifiKey wkey;
// Quoted SSIDs are the only one valid at this stage
try {
wkey = new WifiKey("\"" + result.SSID + "\"", result.BSSID);
} catch (IllegalArgumentException e) {
logDbg("AutoJoinController: received badly encoded SSID=[" + result.SSID +
"] ->skipping this network");
wkey = null;
}
if (wkey != null) {
NetworkKey nkey = new NetworkKey(wkey);
//if we don't know this scan result then request a score from the scorer
unknownScanResults.add(nkey);
}
if (VDBG) {
String cap = "";
if (result.capabilities != null)
cap = result.capabilities;
logDbg(result.SSID + " " + result.BSSID + " rssi="
+ result.level + " cap " + cap + " tsf " + result.timestamp + " is not scored");
}
}
int addToScanCache(List<ScanDetail> scanList) {
int numScanResultsKnown = 0; // Record number of scan results we knew about
WifiConfiguration associatedConfig = null;
boolean didAssociate = false;
long now = System.currentTimeMillis();
ArrayList<NetworkKey> unknownScanResults = new ArrayList<NetworkKey>();
for (ScanDetail scanDetail : scanList) {
ScanResult result = scanDetail.getScanResult();
if (result.SSID == null) continue;
if (VDBG) {
logDbg(" addToScanCache " + result.SSID + " " + result.BSSID
+ " tsf=" + result.timestamp
+ " now= " + now + " uptime=" + SystemClock.uptimeMillis()
+ " elapsed=" + SystemClock.elapsedRealtime());
}
// Make sure we record the last time we saw this result
scanDetail.setSeen();
averageRssiAndRemoveFromCache(result);
if (!mNetworkScoreCache.isScoredNetwork(result)) {
addToUnscoredNetworks(result, unknownScanResults);
} else {
if (VDBG) {
String cap = "";
if (result.capabilities != null)
cap = result.capabilities;
int score = mNetworkScoreCache.getNetworkScore(result);
logDbg(result.SSID + " " + result.BSSID + " rssi="
+ result.level + " cap " + cap + " is scored : " + score);
}
}
// scanResultCache.put(result.BSSID, new ScanResult(result));
scanResultCache.put(result.BSSID, scanDetail);
// Add this BSSID to the scanResultCache of a Saved WifiConfiguration
didAssociate = mWifiConfigStore.updateSavedNetworkHistory(scanDetail);
// If not successful, try to associate this BSSID to an existing Saved WifiConfiguration
if (!didAssociate) {
// We couldn't associate the scan result to a Saved WifiConfiguration
// Hence it is untrusted
result.untrusted = true;
} else {
// If the scan result has been blacklisted fir 18 hours -> unblacklist
if ((now - result.blackListTimestamp) > loseBlackListHardMilli) {
result.setAutoJoinStatus(ScanResult.ENABLED);
}
}
if (didAssociate) {
numScanResultsKnown++;
result.isAutoJoinCandidate++;
} else {
result.isAutoJoinCandidate = 0;
}
}
if (unknownScanResults.size() != 0) {
NetworkKey[] newKeys =
unknownScanResults.toArray(new NetworkKey[unknownScanResults.size()]);
// Kick the score manager, we will get updated scores asynchronously
scoreManager.requestScores(newKeys);
}
return numScanResultsKnown;
}
void logDbg(String message) {
logDbg(message, false);
}
void logDbg(String message, boolean stackTrace) {
if (stackTrace) {
Log.d(TAG, message + " stack:"
+ Thread.currentThread().getStackTrace()[2].getMethodName() + " - "
+ Thread.currentThread().getStackTrace()[3].getMethodName() + " - "
+ Thread.currentThread().getStackTrace()[4].getMethodName() + " - "
+ Thread.currentThread().getStackTrace()[5].getMethodName());
} else {
Log.d(TAG, message);
}
}
// Called directly from WifiStateMachine
int newSupplicantResults(boolean doAutoJoin) {
int numScanResultsKnown;
List<ScanDetail> scanList = mWifiStateMachine.getScanResultsListNoCopyUnsync();
numScanResultsKnown = addToScanCache(scanList);
ageScanResultsOut(mScanResultMaximumAge);
if (DBG) {
logDbg("newSupplicantResults size=" + Integer.valueOf(scanResultCache.size())
+ " known=" + numScanResultsKnown + " "
+ doAutoJoin);
}
if (doAutoJoin) {
attemptAutoJoin();
}
mWifiConfigStore.writeKnownNetworkHistory(false);
return numScanResultsKnown;
}
/**
* Not used at the moment
* should be a call back from WifiScanner HAL ??
* this function is not hooked and working yet, it will receive scan results from WifiScanners
* with the list of IEs,then populate the capabilities by parsing the IEs and inject the scan
* results as normal.
*/
void newHalScanResults() {
List<ScanDetail> scanList = null;//mWifiScanner.syncGetScanResultsList();
String akm = WifiParser.parse_akm(null, null);
logDbg(akm);
addToScanCache(scanList);
ageScanResultsOut(0);
attemptAutoJoin();
mWifiConfigStore.writeKnownNetworkHistory(false);
}
/**
* network link quality changed, called directly from WifiTrafficPoller,
* or by listening to Link Quality intent
*/
void linkQualitySignificantChange() {
attemptAutoJoin();
}
/**
* compare a WifiConfiguration against the current network, return a delta score
* If not associated, and the candidate will always be better
* For instance if the candidate is a home network versus an unknown public wifi,
* the delta will be infinite, else compare Kepler scores etc…
* Negatve return values from this functions are meaningless per se, just trying to
* keep them distinct for debug purpose (i.e. -1, -2 etc...)
*/
private int compareNetwork(WifiConfiguration candidate,
String lastSelectedConfiguration) {
if (candidate == null)
return -3;
WifiConfiguration currentNetwork = mWifiStateMachine.getCurrentWifiConfiguration();
if (currentNetwork == null) {
// Return any absurdly high score, if we are not connected there is no current
// network to...
return 1000;
}
if (candidate.configKey(true).equals(currentNetwork.configKey(true))) {
return -2;
}
if (DBG) {
logDbg("compareNetwork will compare " + candidate.configKey()
+ " with current " + currentNetwork.configKey());
}
int order = compareWifiConfigurations(currentNetwork, candidate);
// The lastSelectedConfiguration is the configuration the user has manually selected
// thru WifiPicker, or that a 3rd party app asked us to connect to via the
// enableNetwork with disableOthers=true WifiManager API
// As this is a direct user choice, we strongly prefer this configuration,
// hence give +/-100
if ((lastSelectedConfiguration != null)
&& currentNetwork.configKey().equals(lastSelectedConfiguration)) {
// currentNetwork is the last selected configuration,
// so keep it above connect choices (+/-60) and
// above RSSI/scorer based selection of linked configuration (+/- 50)
// by reducing order by -100
order = order - 100;
if (VDBG) {
logDbg(" ...and prefers -100 " + currentNetwork.configKey()
+ " over " + candidate.configKey()
+ " because it is the last selected -> "
+ Integer.toString(order));
}
} else if ((lastSelectedConfiguration != null)
&& candidate.configKey().equals(lastSelectedConfiguration)) {
// candidate is the last selected configuration,
// so keep it above connect choices (+/-60) and
// above RSSI/scorer based selection of linked configuration (+/- 50)
// by increasing order by +100
order = order + 100;
if (VDBG) {
logDbg(" ...and prefers +100 " + candidate.configKey()
+ " over " + currentNetwork.configKey()
+ " because it is the last selected -> "
+ Integer.toString(order));
}
}
return order;
}
/**
* update the network history fields fo that configuration
* - if userTriggered, we mark the configuration as "non selfAdded" since the user has seen it
* and took over management
* - if it is a "connect", remember which network were there at the point of the connect, so
* as those networks get a relative lower score than the selected configuration
*
* @param netId
* @param userTriggered : if the update come from WiFiManager
* @param connect : if the update includes a connect
*/
public void updateConfigurationHistory(int netId, boolean userTriggered, boolean connect) {
WifiConfiguration selected = mWifiConfigStore.getWifiConfiguration(netId);
if (selected == null) {
logDbg("updateConfigurationHistory nid=" + netId + " no selected configuration!");
return;
}
if (selected.SSID == null) {
logDbg("updateConfigurationHistory nid=" + netId +
" no SSID in selected configuration!");
return;
}
if (userTriggered) {
// Reenable autojoin for this network,
// since the user want to connect to this configuration
selected.setAutoJoinStatus(WifiConfiguration.AUTO_JOIN_ENABLED);
selected.selfAdded = false;
selected.dirty = true;
}
if (DBG && userTriggered) {
if (selected.connectChoices != null) {
logDbg("updateConfigurationHistory will update "
+ Integer.toString(netId) + " now: "
+ Integer.toString(selected.connectChoices.size())
+ " uid=" + Integer.toString(selected.creatorUid), true);
} else {
logDbg("updateConfigurationHistory will update "
+ Integer.toString(netId)
+ " uid=" + Integer.toString(selected.creatorUid), true);
}
}
if (connect && userTriggered) {
boolean found = false;
int choice = 0;
int size = 0;
// Reset the triggered disabled count, because user wanted to connect to this
// configuration, and we were not.
selected.numUserTriggeredWifiDisableLowRSSI = 0;
selected.numUserTriggeredWifiDisableBadRSSI = 0;
selected.numUserTriggeredWifiDisableNotHighRSSI = 0;
selected.numUserTriggeredJoinAttempts++;
List<WifiConfiguration> networks =
mWifiConfigStore.getRecentConfiguredNetworks(12000, false);
if (networks != null) size = networks.size();
logDbg("updateConfigurationHistory found " + size + " networks");
if (networks != null) {
for (WifiConfiguration config : networks) {
if (DBG) {
logDbg("updateConfigurationHistory got " + config.SSID + " nid="
+ Integer.toString(config.networkId));
}
if (selected.configKey(true).equals(config.configKey(true))) {
found = true;
continue;
}
// If the selection was made while config was visible with reasonably good RSSI
// then register the user preference, else ignore
if (config.visibility == null ||
(config.visibility.rssi24 < mWifiConfigStore.thresholdBadRssi24.get()
&& config.visibility.rssi5 < mWifiConfigStore.thresholdBadRssi5.get())
) {
continue;
}
choice = MAX_RSSI_DELTA + 10; // Make sure the choice overrides the RSSI diff
// The selected configuration was preferred over a recently seen config
// hence remember the user's choice:
// add the recently seen config to the selected's connectChoices array
if (selected.connectChoices == null) {
selected.connectChoices = new HashMap<String, Integer>();
}
logDbg("updateConfigurationHistory add a choice " + selected.configKey(true)
+ " over " + config.configKey(true)
+ " choice " + Integer.toString(choice));
// Add the visible config to the selected's connect choice list
selected.connectChoices.put(config.configKey(true), choice);
if (config.connectChoices != null) {
if (VDBG) {
logDbg("updateConfigurationHistory will remove "
+ selected.configKey(true) + " from " + config.configKey(true));
}
// Remove the selected from the recently seen config's connectChoice list
config.connectChoices.remove(selected.configKey(true));
if (selected.linkedConfigurations != null) {
// Remove the selected's linked configuration from the
// recently seen config's connectChoice list
for (String key : selected.linkedConfigurations.keySet()) {
config.connectChoices.remove(key);
}
}
}
}
if (found == false) {
// We haven't found the configuration that the user just selected in our
// scan cache.
// In that case we will need a new scan before attempting to connect to this
// configuration anyhow and thus we can process the scan results then.
logDbg("updateConfigurationHistory try to connect to an old network!! : "
+ selected.configKey());
}
if (selected.connectChoices != null) {
if (VDBG)
logDbg("updateConfigurationHistory " + Integer.toString(netId)
+ " now: " + Integer.toString(selected.connectChoices.size()));
}
}
}
// TODO: write only if something changed
if (userTriggered || connect) {
mWifiConfigStore.writeKnownNetworkHistory(false);
}
}
int getConnectChoice(WifiConfiguration source, WifiConfiguration target, boolean strict) {
int choice = 0;
if (source == null || target == null) {
return 0;
}
if (source.connectChoices != null
&& source.connectChoices.containsKey(target.configKey(true))) {
Integer val = source.connectChoices.get(target.configKey(true));
if (val != null) {
choice = val;
}
} else if (source.linkedConfigurations != null) {
for (String key : source.linkedConfigurations.keySet()) {
WifiConfiguration config = mWifiConfigStore.getWifiConfiguration(key);
if (config != null) {
if (config.connectChoices != null) {
Integer val = config.connectChoices.get(target.configKey(true));
if (val != null) {
choice = val;
}
}
}
}
}
if (!strict && choice == 0) {
// We didn't find the connect choice; fallback to some default choices
int sourceScore = getSecurityScore(source);
int targetScore = getSecurityScore(target);
choice = sourceScore - targetScore;
}
return choice;
}
int compareWifiConfigurationsFromVisibility(WifiConfiguration.Visibility a, int aRssiBoost,
String dbgA, WifiConfiguration.Visibility b, int bRssiBoost, String dbgB) {
int aRssiBoost5 = 0; // 5GHz RSSI boost to apply for purpose band selection (5GHz pref)
int bRssiBoost5 = 0; // 5GHz RSSI boost to apply for purpose band selection (5GHz pref)
int aScore = 0;
int bScore = 0;
boolean aPrefers5GHz = false;
boolean bPrefers5GHz = false;
/**
* Calculate a boost to apply to RSSI value of configuration we want to join on 5GHz:
* Boost RSSI value of 5GHz bands iff the base value is better than threshold,
* penalize the RSSI value of 5GHz band iff the base value is lower than threshold
* This implements band preference where we prefer 5GHz if RSSI5 is good enough, whereas
* we prefer 2.4GHz otherwise.
*/
aRssiBoost5 = rssiBoostFrom5GHzRssi(a.rssi5, dbgA + "->");
bRssiBoost5 = rssiBoostFrom5GHzRssi(b.rssi5, dbgB + "->");
// Select which band to use for a
if (a.rssi5 + aRssiBoost5 > a.rssi24) {
// Prefer a's 5GHz
aPrefers5GHz = true;
}
// Select which band to use for b
if (b.rssi5 + bRssiBoost5 > b.rssi24) {
// Prefer b's 5GHz
bPrefers5GHz = true;
}
if (aPrefers5GHz) {
if (bPrefers5GHz) {
// If both a and b are on 5GHz then we don't apply the 5GHz RSSI boost to either
// one, but directly compare the RSSI values, this improves stability,
// since the 5GHz RSSI boost can introduce large fluctuations
aScore = a.rssi5 + aRssiBoost;
} else {
// If only a is on 5GHz, then apply the 5GHz preference boost to a
aScore = a.rssi5 + aRssiBoost + aRssiBoost5;
}
} else {
aScore = a.rssi24 + aRssiBoost;
}
if (bPrefers5GHz) {
if (aPrefers5GHz) {
// If both a and b are on 5GHz then we don't apply the 5GHz RSSI boost to either
// one, but directly compare the RSSI values, this improves stability,
// since the 5GHz RSSI boost can introduce large fluctuations
bScore = b.rssi5 + bRssiBoost;
} else {
// If only b is on 5GHz, then apply the 5GHz preference boost to b
bScore = b.rssi5 + bRssiBoost + bRssiBoost5;
}
} else {
bScore = b.rssi24 + bRssiBoost;
}
if (VDBG) {
logDbg(" " + dbgA + " is5=" + aPrefers5GHz + " score=" + aScore
+ " " + dbgB + " is5=" + bPrefers5GHz + " score=" + bScore);
}
// Debug only, record RSSI comparison parameters
if (a != null) {
a.score = aScore;
a.currentNetworkBoost = aRssiBoost;
a.bandPreferenceBoost = aRssiBoost5;
}
if (b != null) {
b.score = bScore;
b.currentNetworkBoost = bRssiBoost;
b.bandPreferenceBoost = bRssiBoost5;
}
// Compare a and b
// If a score is higher then a > b and the order is descending (negative)
// If b score is higher then a < b and the order is ascending (positive)
return bScore - aScore;
}
// Compare WifiConfiguration by RSSI, and return a comparison value in the range [-50, +50]
// The result represents "approximately" an RSSI difference measured in dBM
// Adjusted with various parameters:
// +) current network gets a +15 boost
// +) 5GHz signal, if they are strong enough, get a +15 or +25 boost, representing the
// fact that at short range we prefer 5GHz band as it is cleaner of interference and
// provides for wider channels
int compareWifiConfigurationsRSSI(WifiConfiguration a, WifiConfiguration b,
String currentConfiguration) {
int order = 0;
// Boost used so as to favor current config
int aRssiBoost = 0;
int bRssiBoost = 0;
// Retrieve the visibility
WifiConfiguration.Visibility astatus = a.visibility;
WifiConfiguration.Visibility bstatus = b.visibility;
if (astatus == null || bstatus == null) {
// Error visibility wasn't set
logDbg(" compareWifiConfigurations NULL band status!");
return 0;
}
// Apply Hysteresis, boost RSSI of current configuration
if (null != currentConfiguration) {
if (a.configKey().equals(currentConfiguration)) {
aRssiBoost = mWifiConfigStore.currentNetworkBoost;
} else if (b.configKey().equals(currentConfiguration)) {
bRssiBoost = mWifiConfigStore.currentNetworkBoost;
}
}
if (VDBG) {
logDbg(" compareWifiConfigurationsRSSI: " + a.configKey()
+ " rssi=" + Integer.toString(astatus.rssi24)
+ "," + Integer.toString(astatus.rssi5)
+ " boost=" + Integer.toString(aRssiBoost)
+ " " + b.configKey() + " rssi="
+ Integer.toString(bstatus.rssi24) + ","
+ Integer.toString(bstatus.rssi5)
+ " boost=" + Integer.toString(bRssiBoost)
);
}
order = compareWifiConfigurationsFromVisibility(
a.visibility, aRssiBoost, a.configKey(),
b.visibility, bRssiBoost, b.configKey());
// Normalize the order to [-50, +50] = [ -MAX_RSSI_DELTA, MAX_RSSI_DELTA]
if (order > MAX_RSSI_DELTA) order = MAX_RSSI_DELTA;
else if (order < -MAX_RSSI_DELTA) order = -MAX_RSSI_DELTA;
if (VDBG) {
String prefer = " = ";
if (order > 0) {
prefer = " < "; // Ascending
} else if (order < 0) {
prefer = " > "; // Descending
}
logDbg(" compareWifiConfigurationsRSSI " + a.configKey()
+ " rssi=(" + a.visibility.rssi24
+ "," + a.visibility.rssi5
+ ") num=(" + a.visibility.num24
+ "," + a.visibility.num5 + ")"
+ prefer + b.configKey()
+ " rssi=(" + b.visibility.rssi24
+ "," + b.visibility.rssi5
+ ") num=(" + b.visibility.num24
+ "," + b.visibility.num5 + ")"
+ " -> " + order);
}
return order;
}
/**
* b/18490330 only use scorer for untrusted networks
* <p/>
* int compareWifiConfigurationsWithScorer(WifiConfiguration a, WifiConfiguration b) {
* <p/>
* boolean aIsActive = false;
* boolean bIsActive = false;
* <p/>
* // Apply Hysteresis : boost RSSI of current configuration before
* // looking up the score
* if (null != mCurrentConfigurationKey) {
* if (a.configKey().equals(mCurrentConfigurationKey)) {
* aIsActive = true;
* } else if (b.configKey().equals(mCurrentConfigurationKey)) {
* bIsActive = true;
* }
* }
* int scoreA = getConfigNetworkScore(a, mScanResultAutoJoinAge, aIsActive);
* int scoreB = getConfigNetworkScore(b, mScanResultAutoJoinAge, bIsActive);
* <p/>
* // Both configurations need to have a score for the scorer to be used
* // ...and the scores need to be different:-)
* if (scoreA == WifiNetworkScoreCache.INVALID_NETWORK_SCORE
* || scoreB == WifiNetworkScoreCache.INVALID_NETWORK_SCORE) {
* if (VDBG) {
* logDbg(" compareWifiConfigurationsWithScorer no-scores: "
* + a.configKey()
* + " "
* + b.configKey());
* }
* return 0;
* }
* <p/>
* if (VDBG) {
* String prefer = " = ";
* if (scoreA < scoreB) {
* prefer = " < ";
* } if (scoreA > scoreB) {
* prefer = " > ";
* }
* logDbg(" compareWifiConfigurationsWithScorer " + a.configKey()
* + " rssi=(" + a.visibility.rssi24
* + "," + a.visibility.rssi5
* + ") num=(" + a.visibility.num24
* + "," + a.visibility.num5 + ")"
* + " sc=" + scoreA
* + prefer + b.configKey()
* + " rssi=(" + b.visibility.rssi24
* + "," + b.visibility.rssi5
* + ") num=(" + b.visibility.num24
* + "," + b.visibility.num5 + ")"
* + " sc=" + scoreB
* + " -> " + Integer.toString(scoreB - scoreA));
* }
* <p/>
* // If scoreA > scoreB, the comparison is descending hence the return value is negative
* return scoreB - scoreA;
* }
*/
int getSecurityScore(WifiConfiguration config) {
if (TextUtils.isEmpty(config.SSID) == false) {
if (config.allowedKeyManagement.get(KeyMgmt.WPA_EAP)
|| config.allowedKeyManagement.get(KeyMgmt.WPA_PSK)
|| config.allowedKeyManagement.get(KeyMgmt.WPA2_PSK)) {
/* enterprise or PSK networks get highest score */
return 100;
} else if (config.allowedKeyManagement.get(KeyMgmt.NONE)) {
/* open networks have lowest score */
return 33;
}
} else if (TextUtils.isEmpty(config.FQDN) == false) {
/* passpoint networks have medium preference */
return 66;
}
/* bad network */
return 0;
}
int compareWifiConfigurations(WifiConfiguration a, WifiConfiguration b) {
int order = 0;
boolean linked = false;
if ((a.linkedConfigurations != null) && (b.linkedConfigurations != null)
&& (a.autoJoinStatus == WifiConfiguration.AUTO_JOIN_ENABLED)
&& (b.autoJoinStatus == WifiConfiguration.AUTO_JOIN_ENABLED)) {
if ((a.linkedConfigurations.get(b.configKey(true)) != null)
&& (b.linkedConfigurations.get(a.configKey(true)) != null)) {
linked = true;
}
}
if (a.ephemeral && b.ephemeral == false) {
if (VDBG) {
logDbg(" compareWifiConfigurations ephemeral and prefers " + b.configKey()
+ " over " + a.configKey());
}
return 1; // b is of higher priority - ascending
}
if (b.ephemeral && a.ephemeral == false) {
if (VDBG) {
logDbg(" compareWifiConfigurations ephemeral and prefers " + a.configKey()
+ " over " + b.configKey());
}
return -1; // a is of higher priority - descending
}
// Apply RSSI, in the range [-5, +5]
// after band adjustment, +n difference roughly corresponds to +10xn dBm
order = order + compareWifiConfigurationsRSSI(a, b, mCurrentConfigurationKey);
// If the configurations are not linked, compare by user's choice, only a
// very high RSSI difference can then override the choice
if (!linked) {
int choice;
choice = getConnectChoice(a, b, false);
if (choice > 0) {
// a is of higher priority - descending
order = order - choice;
if (VDBG) {
logDbg(" compareWifiConfigurations prefers " + a.configKey()
+ " over " + b.configKey()
+ " due to user choice of " + choice
+ " order -> " + Integer.toString(order));
}
if (a.visibility != null) {
a.visibility.lastChoiceBoost = choice;
a.visibility.lastChoiceConfig = b.configKey();
}
}
choice = getConnectChoice(b, a, false);
if (choice > 0) {
// a is of lower priority - ascending
order = order + choice;
if (VDBG) {
logDbg(" compareWifiConfigurations prefers " + b.configKey() + " over "
+ a.configKey() + " due to user choice of " + choice
+ " order ->" + Integer.toString(order));
}
if (b.visibility != null) {
b.visibility.lastChoiceBoost = choice;
b.visibility.lastChoiceConfig = a.configKey();
}
}
}
if (order == 0) {
// We don't know anything - pick the last seen i.e. K behavior
// we should do this only for recently picked configurations
if (a.priority > b.priority) {
// a is of higher priority - descending
if (VDBG) {
logDbg(" compareWifiConfigurations prefers -1 " + a.configKey() + " over "
+ b.configKey() + " due to priority");
}
order = -1;
} else if (a.priority < b.priority) {
// a is of lower priority - ascending
if (VDBG) {
logDbg(" compareWifiConfigurations prefers +1 " + b.configKey() + " over "
+ a.configKey() + " due to priority");
}
order = 1;
}
}
String sorder = " == ";
if (order > 0) {
sorder = " < ";
} else if (order < 0) {
sorder = " > ";
}
if (VDBG) {
logDbg("compareWifiConfigurations: " + a.configKey() + sorder
+ b.configKey() + " order " + Integer.toString(order));
}
return order;
}
boolean isBadCandidate(int rssi5, int rssi24) {
return (rssi5 < -80 && rssi24 < -90);
}
/*
int compareWifiConfigurationsTop(WifiConfiguration a, WifiConfiguration b) {
int scorerOrder = compareWifiConfigurationsWithScorer(a, b);
int order = compareWifiConfigurations(a, b);
if (scorerOrder * order < 0) {
if (VDBG) {
logDbg(" -> compareWifiConfigurationsTop: " +
"scorer override " + scorerOrder + " " + order);
}
// For debugging purpose, remember that an override happened
// during that autojoin Attempt
didOverride = true;
a.numScorerOverride++;
b.numScorerOverride++;
}
if (scorerOrder != 0) {
// If the scorer came up with a result then use the scorer's result, else use
// the order provided by the base comparison function
order = scorerOrder;
}
return order;
}
*/
public int rssiBoostFrom5GHzRssi(int rssi, String dbg) {
if (!mWifiConfigStore.enable5GHzPreference) {
return 0;
}
if (rssi
> mWifiConfigStore.bandPreferenceBoostThreshold5.get()) {
// Boost by 2 dB for each point
// Start boosting at -65
// Boost by 20 if above -55
// Boost by 40 if abore -45
int boost = mWifiConfigStore.bandPreferenceBoostFactor5
* (rssi - mWifiConfigStore.bandPreferenceBoostThreshold5.get());
if (boost > 50) {
// 50 dB boost allows jumping from 2.4 to 5GHz
// consistently
boost = 50;
}
if (VDBG && dbg != null) {
logDbg(" " + dbg + ": rssi5 " + rssi + " 5GHz-boost " + boost);
}
return boost;
}
if (rssi
< mWifiConfigStore.bandPreferencePenaltyThreshold5.get()) {
// penalize if < -75
int boost = mWifiConfigStore.bandPreferencePenaltyFactor5
* (rssi - mWifiConfigStore.bandPreferencePenaltyThreshold5.get());
return boost;
}
return 0;
}
/**
* attemptRoam() function implements the core of the same SSID switching algorithm
* <p/>
* Run thru all recent scan result of a WifiConfiguration and select the
* best one.
*/
public ScanResult attemptRoam(ScanResult a,
WifiConfiguration current, int age, String currentBSSID) {
if (current == null) {
if (VDBG) {
logDbg("attemptRoam not associated");
}
return a;
}
ScanDetailCache scanDetailCache =
mWifiConfigStore.getScanDetailCache(current);
if (scanDetailCache == null) {
if (VDBG) {
logDbg("attemptRoam no scan cache");
}
return a;
}
if (scanDetailCache.size() > 6) {
if (VDBG) {
logDbg("attemptRoam scan cache size "
+ scanDetailCache.size() + " --> bail");
}
// Implement same SSID roaming only for configurations
// that have less than 4 BSSIDs
return a;
}
if (current.BSSID != null && !current.BSSID.equals("any")) {
if (DBG) {
logDbg("attemptRoam() BSSID is set "
+ current.BSSID + " -> bail");
}
return a;
}
// Determine which BSSID we want to associate to, taking account
// relative strength of 5 and 2.4 GHz BSSIDs
long nowMs = System.currentTimeMillis();
for (ScanDetail sd : scanDetailCache.values()) {
ScanResult b = sd.getScanResult();
int bRssiBoost5 = 0;
int aRssiBoost5 = 0;
int bRssiBoost = 0;
int aRssiBoost = 0;
if ((sd.getSeen() == 0) || (b.BSSID == null)
|| ((nowMs - sd.getSeen()) > age)
|| b.autoJoinStatus != ScanResult.ENABLED
|| b.numIpConfigFailures > 8) {
continue;
}
// Pick first one
if (a == null) {
a = b;
continue;
}
if (b.numIpConfigFailures < (a.numIpConfigFailures - 1)) {
// Prefer a BSSID that doesn't have less number of Ip config failures
logDbg("attemptRoam: "
+ b.BSSID + " rssi=" + b.level + " ipfail=" + b.numIpConfigFailures
+ " freq=" + b.frequency
+ " > "
+ a.BSSID + " rssi=" + a.level + " ipfail=" + a.numIpConfigFailures
+ " freq=" + a.frequency);
a = b;
continue;
}
// Apply hysteresis: we favor the currentBSSID by giving it a boost
if (currentBSSID != null && currentBSSID.equals(b.BSSID)) {
// Reduce the benefit of hysteresis if RSSI <= -75
if (b.level <= mWifiConfigStore.bandPreferencePenaltyThreshold5.get()) {
bRssiBoost = mWifiConfigStore.associatedHysteresisLow;
} else {
bRssiBoost = mWifiConfigStore.associatedHysteresisHigh;
}
}
if (currentBSSID != null && currentBSSID.equals(a.BSSID)) {
if (a.level <= mWifiConfigStore.bandPreferencePenaltyThreshold5.get()) {
// Reduce the benefit of hysteresis if RSSI <= -75
aRssiBoost = mWifiConfigStore.associatedHysteresisLow;
} else {
aRssiBoost = mWifiConfigStore.associatedHysteresisHigh;
}
}
// Favor 5GHz: give a boost to 5GHz BSSIDs, with a slightly progressive curve
// Boost the BSSID if it is on 5GHz, above a threshold
// But penalize it if it is on 5GHz and below threshold
//
// With he current threshold values, 5GHz network with RSSI above -55
// Are given a boost of 30DB which is enough to overcome the current BSSID
// hysteresis (+14) plus 2.4/5 GHz signal strength difference on most cases
//
// The "current BSSID" Boost must be added to the BSSID's level so as to introduce\
// soem amount of hysteresis
if (b.is5GHz()) {
bRssiBoost5 = rssiBoostFrom5GHzRssi(b.level + bRssiBoost, b.BSSID);
}
if (a.is5GHz()) {
aRssiBoost5 = rssiBoostFrom5GHzRssi(a.level + aRssiBoost, a.BSSID);
}
if (VDBG) {
String comp = " < ";
if (b.level + bRssiBoost + bRssiBoost5 > a.level + aRssiBoost + aRssiBoost5) {
comp = " > ";
}
logDbg("attemptRoam: "
+ b.BSSID + " rssi=" + b.level + " boost=" + Integer.toString(bRssiBoost)
+ "/" + Integer.toString(bRssiBoost5) + " freq=" + b.frequency
+ comp
+ a.BSSID + " rssi=" + a.level + " boost=" + Integer.toString(aRssiBoost)
+ "/" + Integer.toString(aRssiBoost5) + " freq=" + a.frequency);
}
// Compare the RSSIs after applying the hysteresis boost and the 5GHz
// boost if applicable
if (b.level + bRssiBoost + bRssiBoost5 > a.level + aRssiBoost + aRssiBoost5) {
// b is the better BSSID
a = b;
}
}
if (a != null) {
if (VDBG) {
StringBuilder sb = new StringBuilder();
sb.append("attemptRoam: " + current.configKey() +
" Found " + a.BSSID + " rssi=" + a.level + " freq=" + a.frequency);
if (currentBSSID != null) {
sb.append(" Current: " + currentBSSID);
}
sb.append("\n");
logDbg(sb.toString());
}
}
return a;
}
/**
* getNetworkScore()
* <p/>
* if scorer is present, get the network score of a WifiConfiguration
* <p/>
* Note: this should be merge with setVisibility
*
* @param config
* @return score
*/
int getConfigNetworkScore(WifiConfiguration config, int age, boolean isActive) {
if (mNetworkScoreCache == null) {
if (VDBG) {
logDbg(" getConfigNetworkScore for " + config.configKey()
+ " -> no scorer, hence no scores");
}
return WifiNetworkScoreCache.INVALID_NETWORK_SCORE;
}
if (mWifiConfigStore.getScanDetailCache(config) == null) {
if (VDBG) {
logDbg(" getConfigNetworkScore for " + config.configKey()
+ " -> no scan cache");
}
return WifiNetworkScoreCache.INVALID_NETWORK_SCORE;
}
// Get current date
long nowMs = System.currentTimeMillis();
int startScore = -10000;
// Run thru all cached scan results
for (ScanDetail sd : mWifiConfigStore.getScanDetailCache(config).values()) {
ScanResult result = sd.getScanResult();
if ((nowMs - sd.getSeen()) < age) {
int sc = mNetworkScoreCache.getNetworkScore(result, isActive);
if (sc > startScore) {
startScore = sc;
}
}
}
if (startScore == -10000) {
startScore = WifiNetworkScoreCache.INVALID_NETWORK_SCORE;
}
if (VDBG) {
if (startScore == WifiNetworkScoreCache.INVALID_NETWORK_SCORE) {
logDbg(" getConfigNetworkScore for " + config.configKey()
+ " -> no available score");
} else {
logDbg(" getConfigNetworkScore for " + config.configKey()
+ " isActive=" + isActive
+ " score = " + Integer.toString(startScore));
}
}
return startScore;
}
/**
* Set whether connections to untrusted connections are allowed.
*/
void setAllowUntrustedConnections(boolean allow) {
boolean changed = mAllowUntrustedConnections != allow;
mAllowUntrustedConnections = allow;
if (changed) {
// Trigger a scan so as to reattempt autojoin
mWifiStateMachine.startScanForUntrustedSettingChange();
}
}
private boolean isOpenNetwork(ScanResult result) {
return !result.capabilities.contains("WEP") &&
!result.capabilities.contains("PSK") &&
!result.capabilities.contains("EAP");
}
private boolean haveRecentlySeenScoredBssid(WifiConfiguration config) {
long ephemeralOutOfRangeTimeoutMs = Settings.Global.getLong(
mContext.getContentResolver(),
Settings.Global.WIFI_EPHEMERAL_OUT_OF_RANGE_TIMEOUT_MS,
DEFAULT_EPHEMERAL_OUT_OF_RANGE_TIMEOUT_MS);
// Check whether the currently selected network has a score curve. If
// ephemeralOutOfRangeTimeoutMs is <= 0, then this is all we check, and we stop here.
// Otherwise, we stop here if the currently selected network has a score. If it doesn't, we
// keep going - it could be that another BSSID is in range (has been seen recently) which
// has a score, even if the one we're immediately connected to doesn't.
ScanResult currentScanResult = mWifiStateMachine.getCurrentScanResult();
boolean currentNetworkHasScoreCurve = currentScanResult != null
&& mNetworkScoreCache.hasScoreCurve(currentScanResult);
if (ephemeralOutOfRangeTimeoutMs <= 0 || currentNetworkHasScoreCurve) {
if (DBG) {
if (currentNetworkHasScoreCurve) {
logDbg("Current network has a score curve, keeping network: "
+ currentScanResult);
} else {
logDbg("Current network has no score curve, giving up: " + config.SSID);
}
}
return currentNetworkHasScoreCurve;
}
if (mWifiConfigStore.getScanDetailCache(config) == null
|| mWifiConfigStore.getScanDetailCache(config).isEmpty()) {
return false;
}
long currentTimeMs = System.currentTimeMillis();
for (ScanDetail sd : mWifiConfigStore.getScanDetailCache(config).values()) {
ScanResult result = sd.getScanResult();
if (currentTimeMs > sd.getSeen()
&& currentTimeMs - sd.getSeen() < ephemeralOutOfRangeTimeoutMs
&& mNetworkScoreCache.hasScoreCurve(result)) {
if (DBG) {
logDbg("Found scored BSSID, keeping network: " + result.BSSID);
}
return true;
}
}
if (DBG) {
logDbg("No recently scored BSSID found, giving up connection: " + config.SSID);
}
return false;
}
// After WifiStateMachine ask the supplicant to associate or reconnect
// we might still obtain scan results from supplicant
// however the supplicant state in the mWifiInfo and supplicant state tracker
// are updated when we get the supplicant state change message which can be
// processed after the SCAN_RESULT message, so at this point the framework doesn't
// know that supplicant is ASSOCIATING.
// A good fix for this race condition would be for the WifiStateMachine to add
// a new transient state where it expects to get the supplicant message indicating
// that it started the association process and within which critical operations
// like autojoin should be deleted.
// This transient state would remove the need for the roam Wathchdog which
// basically does that.
// At the moment, we just query the supplicant state synchronously with the
// mWifiNative.status() command, which allow us to know that
// supplicant has started association process, even though we didnt yet get the
// SUPPLICANT_STATE_CHANGE message.
private static final List<String> ASSOC_STATES = Arrays.asList(
"ASSOCIATING",
"ASSOCIATED",
"FOUR_WAY_HANDSHAKE",
"GROUP_KEY_HANDSHAKE");
private int getNetID(String wpaStatus) {
if (VDBG) {
logDbg("attemptAutoJoin() status=" + wpaStatus);
}
try {
int id = WifiConfiguration.INVALID_NETWORK_ID;
String state = null;
BufferedReader br = new BufferedReader(new StringReader(wpaStatus));
String line;
while ((line = br.readLine()) != null) {
int split = line.indexOf('=');
if (split < 0) {
continue;
}
String name = line.substring(0, split);
if (name.equals("id")) {
try {
id = Integer.parseInt(line.substring(split + 1));
if (state != null) {
break;
}
} catch (NumberFormatException nfe) {
return WifiConfiguration.INVALID_NETWORK_ID;
}
} else if (name.equals("wpa_state")) {
state = line.substring(split + 1);
if (ASSOC_STATES.contains(state)) {
return WifiConfiguration.INVALID_NETWORK_ID;
} else if (id >= 0) {
break;
}
}
}
return id;
} catch (IOException ioe) {
return WifiConfiguration.INVALID_NETWORK_ID; // Won't happen
}
}
private boolean setCurrentConfigurationKey(WifiConfiguration currentConfig,
int supplicantNetId) {
if (currentConfig != null) {
if (supplicantNetId != currentConfig.networkId
// https://b.corp.google.com/issue?id=16484607
// mark this condition as an error only if the mismatched networkId are valid
&& supplicantNetId != WifiConfiguration.INVALID_NETWORK_ID
&& currentConfig.networkId != WifiConfiguration.INVALID_NETWORK_ID) {
logDbg("attemptAutoJoin() ERROR wpa_supplicant out of sync nid="
+ Integer.toString(supplicantNetId) + " WifiStateMachine="
+ Integer.toString(currentConfig.networkId));
mWifiStateMachine.disconnectCommand();
return false;
} else if (currentConfig.ephemeral && (!mAllowUntrustedConnections ||
!haveRecentlySeenScoredBssid(currentConfig))) {
// The current connection is untrusted (the framework added it), but we're either
// no longer allowed to connect to such networks, the score has been nullified
// since we connected, or the scored BSSID has gone out of range.
// Drop the current connection and perform the rest of autojoin.
logDbg("attemptAutoJoin() disconnecting from unwanted ephemeral network");
mWifiStateMachine.disconnectCommand(Process.WIFI_UID,
mAllowUntrustedConnections ? 1 : 0);
return false;
} else {
mCurrentConfigurationKey = currentConfig.configKey();
return true;
}
} else {
// If not invalid, then maybe in the process of associating, skip this attempt
return supplicantNetId == WifiConfiguration.INVALID_NETWORK_ID;
}
}
private void updateBlackListStatus(WifiConfiguration config, long now) {
// Wait for 5 minutes before reenabling config that have known,
// repeated connection or DHCP failures
if (config.disableReason == WifiConfiguration.DISABLED_DHCP_FAILURE
|| config.disableReason
== WifiConfiguration.DISABLED_ASSOCIATION_REJECT
|| config.disableReason
== WifiConfiguration.DISABLED_AUTH_FAILURE) {
if (config.blackListTimestamp == 0
|| (config.blackListTimestamp > now)) {
// Sanitize the timestamp
config.blackListTimestamp = now;
}
if ((now - config.blackListTimestamp) >
mWifiConfigStore.wifiConfigBlacklistMinTimeMilli) {
// Re-enable the WifiConfiguration
config.status = WifiConfiguration.Status.ENABLED;
// Reset the blacklist condition
config.numConnectionFailures = 0;
config.numIpConfigFailures = 0;
config.numAuthFailures = 0;
config.setAutoJoinStatus(WifiConfiguration.AUTO_JOIN_ENABLED);
config.dirty = true;
} else {
if (VDBG) {
long delay = mWifiConfigStore.wifiConfigBlacklistMinTimeMilli
- (now - config.blackListTimestamp);
logDbg("attemptautoJoin " + config.configKey()
+ " dont unblacklist yet, waiting for "
+ delay + " ms");
}
}
}
// Avoid networks disabled because of AUTH failure altogether
if (DBG) {
logDbg("attemptAutoJoin skip candidate due to auto join status "
+ Integer.toString(config.autoJoinStatus) + " key "
+ config.configKey(true)
+ " reason " + config.disableReason);
}
}
boolean underSoftThreshold(WifiConfiguration config) {
return config.visibility.rssi24 < mWifiConfigStore.thresholdUnblacklistThreshold24Soft.get()
&& config.visibility.rssi5 < mWifiConfigStore.thresholdUnblacklistThreshold5Soft.get();
}
boolean underHardThreshold(WifiConfiguration config) {
return config.visibility.rssi24 < mWifiConfigStore.thresholdUnblacklistThreshold24Hard.get()
&& config.visibility.rssi5 < mWifiConfigStore.thresholdUnblacklistThreshold5Hard.get();
}
boolean underThreshold(WifiConfiguration config, int rssi24, int rssi5) {
return config.visibility.rssi24 < rssi24 && config.visibility.rssi5 < rssi5;
}
/**
* attemptAutoJoin() function implements the core of the a network switching algorithm
* Return false if no acceptable networks were found.
*/
boolean attemptAutoJoin() {
boolean found = false;
didOverride = false;
didBailDueToWeakRssi = false;
int networkSwitchType = AUTO_JOIN_IDLE;
int age = mScanResultAutoJoinAge;
long now = System.currentTimeMillis();
String lastSelectedConfiguration = mWifiConfigStore.getLastSelectedConfiguration();
if (lastSelectedConfiguration != null) {
age = 14000;
}
// Reset the currentConfiguration Key, and set it only if WifiStateMachine and
// supplicant agree
mCurrentConfigurationKey = null;
WifiConfiguration currentConfiguration = mWifiStateMachine.getCurrentWifiConfiguration();
WifiConfiguration candidate = null;
// Obtain the subset of recently seen networks
List<WifiConfiguration> list =
mWifiConfigStore.getRecentConfiguredNetworks(age, false);
if (list == null) {
if (VDBG) logDbg("attemptAutoJoin nothing known=" +
mWifiConfigStore.getConfiguredNetworksSize());
return false;
}
// Find the currently connected network: ask the supplicant directly
int supplicantNetId = getNetID(mWifiNative.status(true));
if (DBG) {
String conf = "";
String last = "";
if (currentConfiguration != null) {
conf = " current=" + currentConfiguration.configKey();
}
if (lastSelectedConfiguration != null) {
last = " last=" + lastSelectedConfiguration;
}
logDbg("attemptAutoJoin() num recent config " + Integer.toString(list.size())
+ conf + last
+ " ---> suppNetId=" + Integer.toString(supplicantNetId));
}
if (!setCurrentConfigurationKey(currentConfiguration, supplicantNetId)) {
return false;
}
int currentNetId = -1;
if (currentConfiguration != null) {
// If we are associated to a configuration, it will
// be compared thru the compareNetwork function
currentNetId = currentConfiguration.networkId;
}
/**
* Run thru all visible configurations without looking at the one we
* are currently associated to
* select Best Network candidate from known WifiConfigurations
*/
for (WifiConfiguration config : list) {
if (config.SSID == null) {
continue;
}
if (config.autoJoinStatus >= WifiConfiguration.AUTO_JOIN_DISABLED_ON_AUTH_FAILURE) {
updateBlackListStatus(config, now);
continue;
}
if (config.userApproved == WifiConfiguration.USER_PENDING ||
config.userApproved == WifiConfiguration.USER_BANNED) {
if (DBG) {
logDbg("attemptAutoJoin skip candidate due to user approval status "
+ WifiConfiguration.userApprovedAsString(config.userApproved) + " key "
+ config.configKey(true));
}
continue;
}
// Try to un-blacklist based on elapsed time
if (config.blackListTimestamp > 0) {
if (now < config.blackListTimestamp) {
/**
* looks like there was a change in the system clock since we black listed, and
* timestamp is not meaningful anymore, hence lose it.
* this event should be rare enough so that we still want to lose the black list
*/
config.setAutoJoinStatus(WifiConfiguration.AUTO_JOIN_ENABLED);
} else {
if ((now - config.blackListTimestamp) > loseBlackListHardMilli) {
// Reenable it after 18 hours, i.e. next day
config.setAutoJoinStatus(WifiConfiguration.AUTO_JOIN_ENABLED);
} else if ((now - config.blackListTimestamp) > loseBlackListSoftMilli) {
// Lose blacklisting due to bad link
config.setAutoJoinStatus(config.autoJoinStatus - 8);
}
}
}
if (config.visibility == null) {
continue;
}
// Try to unblacklist based on good visibility
if (underSoftThreshold(config)) {
if (DBG) {
logDbg("attemptAutoJoin do not unblacklist due to low visibility " +
config.configKey() + " status=" + config.autoJoinStatus);
}
} else if (underHardThreshold(config)) {
// If the network is simply temporary disabled, don't allow reconnect until
// RSSI becomes good enough
config.setAutoJoinStatus(config.autoJoinStatus - 1);
if (DBG) {
logDbg("attemptAutoJoin good candidate seen, bumped soft -> status=" +
config.configKey() + " status=" + config.autoJoinStatus);
}
} else {
config.setAutoJoinStatus(config.autoJoinStatus - 3);
if (DBG) {
logDbg("attemptAutoJoin good candidate seen, bumped hard -> status=" +
config.configKey() + " status=" + config.autoJoinStatus);
}
}
if (config.autoJoinStatus >=
WifiConfiguration.AUTO_JOIN_TEMPORARY_DISABLED) {
// Network is blacklisted, skip
if (DBG) {
logDbg("attemptAutoJoin skip blacklisted -> status=" +
config.configKey() + " status=" + config.autoJoinStatus);
}
continue;
}
if (config.networkId == currentNetId) {
if (DBG) {
logDbg("attemptAutoJoin skip current candidate "
+ Integer.toString(currentNetId)
+ " key " + config.configKey(true));
}
continue;
}
boolean isLastSelected = false;
if (lastSelectedConfiguration != null &&
config.configKey().equals(lastSelectedConfiguration)) {
isLastSelected = true;
}
if (config.lastRoamingFailure != 0
&& currentConfiguration != null
&& (lastSelectedConfiguration == null
|| !config.configKey().equals(lastSelectedConfiguration))) {
// Apply blacklisting for roaming to this config if:
// - the target config had a recent roaming failure
// - we are currently associated
// - the target config is not the last selected
if (now > config.lastRoamingFailure
&& (now - config.lastRoamingFailure)
< config.roamingFailureBlackListTimeMilli) {
if (DBG) {
logDbg("compareNetwork not switching to " + config.configKey()
+ " from current " + currentConfiguration.configKey()
+ " because it is blacklisted due to roam failure, "
+ " blacklist remain time = "
+ (now - config.lastRoamingFailure) + " ms");
}
continue;
}
}
int boost = config.autoJoinUseAggressiveJoinAttemptThreshold + weakRssiBailCount;
if (underThreshold(config,
mWifiConfigStore.thresholdInitialAutoJoinAttemptMin24RSSI.get() - boost,
mWifiConfigStore.thresholdInitialAutoJoinAttemptMin5RSSI.get() - boost)) {
if (DBG) {
logDbg("attemptAutoJoin skip due to low visibility " + config.configKey());
}
// Don't try to autojoin a network that is too far but
// If that configuration is a user's choice however, try anyway
if (!isLastSelected) {
config.autoJoinBailedDueToLowRssi = true;
didBailDueToWeakRssi = true;
continue;
} else {
// Next time, try to be a bit more aggressive in auto-joining
if (config.autoJoinUseAggressiveJoinAttemptThreshold
< WifiConfiguration.MAX_INITIAL_AUTO_JOIN_RSSI_BOOST
&& config.autoJoinBailedDueToLowRssi) {
config.autoJoinUseAggressiveJoinAttemptThreshold += 4;
}
}
}
// NOTE: If this condition is updated, update NETWORK_STATUS_UNWANTED_DISABLE_AUTOJOIN.
if (config.numNoInternetAccessReports > 0
&& !isLastSelected
&& !config.validatedInternetAccess) {
// Avoid autoJoining this network because last time we used it, it didn't
// have internet access, and we never manage to validate internet access on this
// network configuration
if (DBG) {
logDbg("attemptAutoJoin skip candidate due to no InternetAccess "
+ config.configKey(true)
+ " num reports " + config.numNoInternetAccessReports);
}
continue;
}
if (DBG) {
String cur = "";
if (candidate != null) {
cur = " current candidate " + candidate.configKey();
}
logDbg("attemptAutoJoin trying id="
+ Integer.toString(config.networkId) + " "
+ config.configKey(true)
+ " status=" + config.autoJoinStatus
+ cur);
}
if (candidate == null) {
candidate = config;
} else {
if (VDBG) {
logDbg("attemptAutoJoin will compare candidate " + candidate.configKey()
+ " with " + config.configKey());
}
int order = compareWifiConfigurations(candidate, config);
if (VDBG) {
logDbg("attemptAutoJoin compareWifiConfigurations returned " + order);
}
// The lastSelectedConfiguration is the configuration the user has manually selected
// thru WifiPicker, or that a 3rd party app asked us to connect to via the
// enableNetwork with disableOthers=true WifiManager API
// As this is a direct user choice, we strongly prefer this configuration,
// hence give +/-100
if ((lastSelectedConfiguration != null)
&& candidate.configKey().equals(lastSelectedConfiguration)) {
// candidate is the last selected configuration,
// so keep it above connect choices (+/-60) and
// above RSSI/scorer based selection of linked configuration (+/- 50)
// by reducing order by -100
order = order - 100;
if (VDBG) {
logDbg(" ...and prefers -100 " + candidate.configKey()
+ " over " + config.configKey()
+ " because it is the last selected -> "
+ Integer.toString(order));
}
} else if ((lastSelectedConfiguration != null)
&& config.configKey().equals(lastSelectedConfiguration)) {
// config is the last selected configuration,
// so keep it above connect choices (+/-60) and
// above RSSI/scorer based selection of linked configuration (+/- 50)
// by increasing order by +100
order = order + 100;
if (VDBG) {
logDbg(" ...and prefers +100 " + config.configKey()
+ " over " + candidate.configKey()
+ " because it is the last selected -> "
+ Integer.toString(order));
}
}
if (order > 0) {
// Ascending : candidate < config
candidate = config;
}
}
}
// Now, go thru scan result to try finding a better untrusted network
if (mNetworkScoreCache != null && mAllowUntrustedConnections) {
int rssi5 = WifiConfiguration.INVALID_RSSI;
int rssi24 = WifiConfiguration.INVALID_RSSI;
if (candidate != null) {
rssi5 = candidate.visibility.rssi5;
rssi24 = candidate.visibility.rssi24;
}
// Get current date
long nowMs = System.currentTimeMillis();
int currentScore = -10000;
// The untrusted network with highest score
ScanDetail untrustedCandidate = null;
// Look for untrusted scored network only if the current candidate is bad
if (isBadCandidate(rssi24, rssi5)) {
for (ScanDetail scanDetail : scanResultCache.values()) {
ScanResult result = scanDetail.getScanResult();
// We look only at untrusted networks with a valid SSID
// A trusted result would have been looked at thru it's Wificonfiguration
if (TextUtils.isEmpty(result.SSID) || !result.untrusted ||
!isOpenNetwork(result)) {
continue;
}
String quotedSSID = "\"" + result.SSID + "\"";
if (mWifiConfigStore.mDeletedEphemeralSSIDs.contains(quotedSSID)) {
// SSID had been Forgotten by user, then don't score it
continue;
}
if ((nowMs - result.seen) < mScanResultAutoJoinAge) {
// Increment usage count for the network
mWifiConnectionStatistics.incrementOrAddUntrusted(quotedSSID, 0, 1);
boolean isActiveNetwork = currentConfiguration != null
&& currentConfiguration.SSID.equals(quotedSSID);
int score = mNetworkScoreCache.getNetworkScore(result, isActiveNetwork);
if (score != WifiNetworkScoreCache.INVALID_NETWORK_SCORE
&& score > currentScore) {
// Highest score: Select this candidate
currentScore = score;
untrustedCandidate = scanDetail;
if (VDBG) {
logDbg("AutoJoinController: found untrusted candidate "
+ result.SSID
+ " RSSI=" + result.level
+ " freq=" + result.frequency
+ " score=" + score);
}
}
}
}
}
if (untrustedCandidate != null) {
// At this point, we have an untrusted network candidate.
// Create the new ephemeral configuration and see if we should switch over
candidate =
mWifiConfigStore.wifiConfigurationFromScanResult(untrustedCandidate);
candidate.allowedKeyManagement.set(KeyMgmt.NONE);
candidate.ephemeral = true;
candidate.dirty = true;
}
}
long lastUnwanted =
System.currentTimeMillis()
- mWifiConfigStore.lastUnwantedNetworkDisconnectTimestamp;
if (candidate == null
&& lastSelectedConfiguration == null
&& currentConfiguration == null
&& didBailDueToWeakRssi
&& (mWifiConfigStore.lastUnwantedNetworkDisconnectTimestamp == 0
|| lastUnwanted > (1000 * 60 * 60 * 24 * 7))
) {
// We are bailing out of autojoin although we are seeing a weak configuration, and
// - we didn't find another valid candidate
// - we are not connected
// - without a user network selection choice
// - ConnectivityService has not triggered an unwanted network disconnect
// on this device for a week (hence most likely there is no SIM card or cellular)
// If all those conditions are met, then boost the RSSI of the weak networks
// that we are seeing so as we will eventually pick one
if (weakRssiBailCount < 10)
weakRssiBailCount += 1;
} else {
if (weakRssiBailCount > 0)
weakRssiBailCount -= 1;
}
/**
* If candidate is found, check the state of the connection so as
* to decide if we should be acting on this candidate and switching over
*/
int networkDelta = compareNetwork(candidate, lastSelectedConfiguration);
if (DBG && candidate != null) {
String doSwitch = "";
String current = "";
if (networkDelta < 0) {
doSwitch = " -> not switching";
}
if (currentConfiguration != null) {
current = " with current " + currentConfiguration.configKey();
}
logDbg("attemptAutoJoin networkSwitching candidate "
+ candidate.configKey()
+ current
+ " linked=" + (currentConfiguration != null
&& currentConfiguration.isLinked(candidate))
+ " : delta="
+ Integer.toString(networkDelta) + " "
+ doSwitch);
}
/**
* Ask WifiStateMachine permission to switch :
* if user is currently streaming voice traffic,
* then we should not be allowed to switch regardless of the delta
*/
if (mWifiStateMachine.shouldSwitchNetwork(networkDelta)) { // !!! JNo: Here!
if (mStaStaSupported) {
logDbg("mStaStaSupported --> error do nothing now ");
} else {
if (currentConfiguration != null && currentConfiguration.isLinked(candidate)) {
networkSwitchType = AUTO_JOIN_EXTENDED_ROAMING;
} else {
networkSwitchType = AUTO_JOIN_OUT_OF_NETWORK_ROAMING;
}
if (DBG) {
logDbg("AutoJoin auto connect with netId "
+ Integer.toString(candidate.networkId)
+ " to " + candidate.configKey());
}
if (didOverride) {
candidate.numScorerOverrideAndSwitchedNetwork++;
}
candidate.numAssociation++;
mWifiConnectionStatistics.numAutoJoinAttempt++;
if (candidate.ephemeral) {
// We found a new candidate that we are going to connect to, then
// increase its connection count
mWifiConnectionStatistics.
incrementOrAddUntrusted(candidate.SSID, 1, 0);
}
if (candidate.BSSID == null || candidate.BSSID.equals("any")) {
// First step we selected the configuration we want to connect to
// Second step: Look for the best Scan result for this configuration
// TODO this algorithm should really be done in one step
String currentBSSID = mWifiStateMachine.getCurrentBSSID();
ScanResult roamCandidate =
attemptRoam(null, candidate, mScanResultAutoJoinAge, null);
if (roamCandidate != null && currentBSSID != null
&& currentBSSID.equals(roamCandidate.BSSID)) {
// Sanity, we were already asociated to that candidate
roamCandidate = null;
}
if (roamCandidate != null && roamCandidate.is5GHz()) {
// If the configuration hasn't a default BSSID selected, and the best
// candidate is 5GHZ, then select this candidate so as WifiStateMachine and
// supplicant will pick it first
candidate.autoJoinBSSID = roamCandidate.BSSID;
if (VDBG) {
logDbg("AutoJoinController: lock to 5GHz "
+ candidate.autoJoinBSSID
+ " RSSI=" + roamCandidate.level
+ " freq=" + roamCandidate.frequency);
}
} else {
// We couldnt find a roam candidate
candidate.autoJoinBSSID = "any";
}
}
mWifiStateMachine.sendMessage(WifiStateMachine.CMD_AUTO_CONNECT,
candidate.networkId, networkSwitchType, candidate);
found = true;
}
}
if (networkSwitchType == AUTO_JOIN_IDLE && !mWifiConfigStore.enableHalBasedPno.get()) {
String currentBSSID = mWifiStateMachine.getCurrentBSSID();
// Attempt same WifiConfiguration roaming
ScanResult roamCandidate =
attemptRoam(null, currentConfiguration, mScanResultAutoJoinAge, currentBSSID);
if (roamCandidate != null && currentBSSID != null
&& currentBSSID.equals(roamCandidate.BSSID)) {
roamCandidate = null;
}
if (roamCandidate != null && mWifiStateMachine.shouldSwitchNetwork(999)) {
if (DBG) {
logDbg("AutoJoin auto roam with netId "
+ Integer.toString(currentConfiguration.networkId)
+ " " + currentConfiguration.configKey() + " to BSSID="
+ roamCandidate.BSSID + " freq=" + roamCandidate.frequency
+ " RSSI=" + roamCandidate.level);
}
networkSwitchType = AUTO_JOIN_ROAMING;
mWifiConnectionStatistics.numAutoRoamAttempt++;
mWifiStateMachine.sendMessage(WifiStateMachine.CMD_AUTO_ROAM,
currentConfiguration.networkId, 1, roamCandidate);
found = true;
}
}
if (VDBG) logDbg("Done attemptAutoJoin status=" + Integer.toString(networkSwitchType));
return found;
}
private void logDenial(String reason, WifiConfiguration config) {
if (!DBG) {
return;
}
logDbg(reason + config.toString());
}
WifiConfiguration getWifiConfiguration(WifiNative.WifiPnoNetwork network) {
if (network.configKey != null) {
return mWifiConfigStore.getWifiConfiguration(network.configKey);
}
return null;
}
ArrayList<WifiNative.WifiPnoNetwork> getPnoList(WifiConfiguration current) {
int size = -1;
ArrayList<WifiNative.WifiPnoNetwork> list = new ArrayList<WifiNative.WifiPnoNetwork>();
if (mWifiConfigStore.mCachedPnoList != null) {
size = mWifiConfigStore.mCachedPnoList.size();
}
if (DBG) {
String s = "";
if (current != null) {
s = " for: " + current.configKey();
}
Log.e(TAG, " get Pno List total size:" + size + s);
}
if (current != null) {
String configKey = current.configKey();
/**
* If we are currently associated to a WifiConfiguration then include
* only those networks that have a higher priority
*/
for (WifiNative.WifiPnoNetwork network : mWifiConfigStore.mCachedPnoList) {
WifiConfiguration config = getWifiConfiguration(network);
if (config == null) {
continue;
}
if (config.autoJoinStatus
>= WifiConfiguration.AUTO_JOIN_DISABLED_NO_CREDENTIALS) {
continue;
}
if (!configKey.equals(network.configKey)) {
int choice = getConnectChoice(config, current, true);
if (choice > 0) {
// config is of higher priority
if (DBG) {
Log.e(TAG, " Pno List adding:" + network.configKey
+ " choice " + choice);
}
list.add(network);
network.rssi_threshold = mWifiConfigStore.thresholdGoodRssi24.get();
}
}
}
} else {
for (WifiNative.WifiPnoNetwork network : mWifiConfigStore.mCachedPnoList) {
WifiConfiguration config = getWifiConfiguration(network);
if (config == null) {
continue;
}
if (config.autoJoinStatus
>= WifiConfiguration.AUTO_JOIN_DISABLED_NO_CREDENTIALS) {
continue;
}
list.add(network);
network.rssi_threshold = mWifiConfigStore.thresholdGoodRssi24.get();
}
}
return list;
}
}