定義
當有兩個或者兩個以上的已經保存的無線網絡可以連接時,系統通過選擇算法來選擇一個最優網絡。
- 在Android L,wifi的自動重連機制是由WifiAutoJoinController 類來實現,核心的方法就是attemptAutoJoin(),
- 然而,android L這個機制和用戶connect的flow會產生沖突,出現了很多的bug,很雞肋。
- 因此,android N對這個auto connect的部分做了大改
實現
auto connect在許多場景都會用到,如開機自動連接、亮屏掃描連接等等,這里我們看亮屏掃描時如何自動重連以及選擇最優網絡的
startPeriodicScan,這個是當屏幕是亮屏的時候,后台一直做scan的操作。
/frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiConnectivityManager.java
// Start a periodic scan when screen is on
private void startPeriodicScan(boolean scanImmediately) {
mPnoScanListener.resetLowRssiNetworkRetryDelay();
// No connectivity scan if auto roaming is disabled.
if (mWifiState == WIFI_STATE_CONNECTED
&& !mConfigManager.getEnableAutoJoinWhenAssociated()) {
return;
}
// Due to b/28020168, timer based single scan will be scheduled
// to provide periodic scan in an exponential backoff fashion.
if (!ENABLE_BACKGROUND_SCAN) {
if (scanImmediately) {
resetLastPeriodicSingleScanTimeStamp();
}
mPeriodicSingleScanInterval = PERIODIC_SCAN_INTERVAL_MS;
startPeriodicSingleScan();
} else {
ScanSettings settings = new ScanSettings();
settings.band = getScanBand();
settings.reportEvents = WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT
| WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN;
settings.numBssidsPerScan = 0;
settings.periodInMs = PERIODIC_SCAN_INTERVAL_MS;
mPeriodicScanListener.clearScanDetails();
mScanner.startBackgroundScan(settings, mPeriodicScanListener, WIFI_WORK_SOURCE);
}
}
看下這里傳入的監聽器:mPeriodicScanListener
/*
WifiConnectivityManager.java (frameworks\opt\net\wifi\service\java\com\android\server\wifi)
*/
// Periodic scan results listener. A periodic scan is initiated when
// screen is on.
private class PeriodicScanListener implements WifiScanner.ScanListener {
private List<ScanDetail> mScanDetails = new ArrayList<ScanDetail>();
public void clearScanDetails() {
mScanDetails.clear();
}
@Override
public void onSuccess() {
localLog("PeriodicScanListener onSuccess");
// reset the count
mScanRestartCount = 0;
}
@Override
public void onFailure(int reason, String description) {
Log.e(TAG, "PeriodicScanListener onFailure:"
+ " reason: " + reason
+ " description: " + description);
// reschedule the scan
if (mScanRestartCount++ < MAX_SCAN_RESTART_ALLOWED) {
scheduleDelayedConnectivityScan(RESTART_SCAN_DELAY_MS);
} else {
mScanRestartCount = 0;
Log.e(TAG, "Failed to successfully start periodic scan for "
+ MAX_SCAN_RESTART_ALLOWED + " times");
}
}
@Override
public void onPeriodChanged(int periodInMs) {
localLog("PeriodicScanListener onPeriodChanged: "
+ "actual scan period " + periodInMs + "ms");
}
@Override
public void onResults(WifiScanner.ScanData[] results) {
//當scan到了結果之后,跑handleScanResults去處理
handleScanResults(mScanDetails, "PeriodicScanListener");
clearScanDetails();
}
@Override
public void onFullResult(ScanResult fullScanResult) {
if (mDbg) {
localLog("PeriodicScanListener onFullResult: "
+ fullScanResult.SSID + " capabilities "
+ fullScanResult.capabilities);
}
mScanDetails.add(ScanDetailUtil.toScanDetail(fullScanResult));
}
}
handleScanResults會去調用QualifiedNetworkSelector.selectQualifiedNetwork去篩選目標ssid,比如:
- RSSI
- 5G or 2.4G
- lastUserSelectedNetworkId
- 等等
我寫了一篇關於selectQualifiedNetwork的詳細分析,大家需要詳細了解評分質量算法的,可以看Android N selectQualifiedNetwork分析
/**
* Handles 'onResult' callbacks for the Periodic, Single & Pno ScanListener.
* Executes selection of potential network candidates, initiation of connection attempt to that
* network.
*
* @return true - if a candidate is selected by QNS
* false - if no candidate is selected by QNS
*/
private boolean handleScanResults(List<ScanDetail> scanDetails, String listenerName) {
localLog(listenerName + " onResults: start QNS");
//調用QualifiedNetworkSelector去篩選目標ssid,這個算法有點復雜。我看下參數就好
①WifiConfiguration candidate =
mQualifiedNetworkSelector.selectQualifiedNetwork(false,
mUntrustedConnectionAllowed, scanDetails,
mStateMachine.isLinkDebouncing(), mStateMachine.isConnected(),
mStateMachine.isDisconnected(),
mStateMachine.isSupplicantTransientState());
mWifiLastResortWatchdog.updateAvailableNetworks(
mQualifiedNetworkSelector.getFilteredScanDetails());
if (candidate != null) {
localLog(listenerName + ": QNS candidate-" + candidate.SSID);
②connectToNetwork(candidate);
return true;
} else {
return false;
}
}
selectQualifiedNetwork:
當它獲得新的掃描結果時,應該在連通性管理器中調用它
檢查是否需要進行網絡選擇。如果需要,檢查所有新的掃描結果和
選擇一個新的符合條件的網絡/BSSID來連接
/**
* ToDo: This should be called in Connectivity Manager when it gets new scan result
* check whether a network slection is needed. If need, check all the new scan results and
* select a new qualified network/BSSID to connect to
*
* @param forceSelectNetwork true -- start a qualified network selection anyway,no matter
* current network is already qualified or not.
* false -- if current network is already qualified, do not do new
* selection
* @param isUntrustedConnectionsAllowed true -- user allow to connect to untrusted network
* false -- user do not allow to connect to untrusted
* network
* @param scanDetails latest scan result obtained (should be connectivity scan only)
* @param isLinkDebouncing true -- Link layer is under debouncing
* false -- Link layer is not under debouncing
* @param isConnected true -- device is connected to an AP currently
* false -- device is not connected to an AP currently
* @param isDisconnected true -- WifiStateMachine is at disconnected state
* false -- WifiStateMachine is not at disconnected state
* @param isSupplicantTransient true -- supplicant is in a transient state
* false -- supplicant is not in a transient state
* @return the qualified network candidate found. If no available candidate, return null
*/
public WifiConfiguration selectQualifiedNetwork(boolean forceSelectNetwork ,
boolean isUntrustedConnectionsAllowed, List<ScanDetail> scanDetails,
boolean isLinkDebouncing, boolean isConnected, boolean isDisconnected,
boolean isSupplicantTransient) {
connectToNetwork(candidate);對我們通過算法算出來的目標ssid發起連接,candidate是一個WifiConfiguration對象。
/**
* Attempt to connect to a network candidate.
*
* Based on the currently connected network, this menthod determines whether we should
* connect or roam to the network candidate recommended by QNS.
*/
private void connectToNetwork(WifiConfiguration candidate) {
ScanResult scanResultCandidate = candidate.getNetworkSelectionStatus().getCandidate();
if (scanResultCandidate == null) {
Log.e(TAG, "connectToNetwork: bad candidate - " + candidate
+ " scanResult: " + scanResultCandidate);
return;
}
String targetBssid = scanResultCandidate.BSSID;
String targetAssociationId = candidate.SSID + " : " + targetBssid;
//如果我們篩選出來的ssid正好是上一次attempt連接的ssid,或者是supplicant現在正在連接的目標ssid,則放棄
// Check if we are already connected or in the process of connecting to the target
// BSSID. mWifiInfo.mBSSID tracks the currently connected BSSID. This is checked just
// in case the firmware automatically roamed to a BSSID different from what QNS
// selected.
if (targetBssid != null
&& (targetBssid.equals(mLastConnectionAttemptBssid)
|| targetBssid.equals(mWifiInfo.getBSSID()))
&& SupplicantState.isConnecting(mWifiInfo.getSupplicantState())) {
localLog("connectToNetwork: Either already connected "
+ "or is connecting to " + targetAssociationId);
return;
}
Long elapsedTimeMillis = mClock.elapsedRealtime();
/**
* This checks the connection attempt rate and recommends whether the connection attempt
* should be skipped or not. This attempts to rate limit the rate of connections to
* prevent us from flapping between networks and draining battery rapidly.
*/
//控制attempt連接速率,如果間隔時間太快,那就放棄
if (!mScreenOn && shouldSkipConnectionAttempt(elapsedTimeMillis)) {
localLog("connectToNetwork: Too many connection attempts. Skipping this attempt!");
mTotalConnectivityAttemptsRateLimited++;
return;
}
//終於要開始連接了,把時間記錄下來noteConnectionAttempt
noteConnectionAttempt(elapsedTimeMillis);
mLastConnectionAttemptBssid = targetBssid;
WifiConfiguration currentConnectedNetwork = mConfigManager
.getWifiConfiguration(mWifiInfo.getNetworkId());
String currentAssociationId = (currentConnectedNetwork == null) ? "Disconnected" :
(mWifiInfo.getSSID() + " : " + mWifiInfo.getBSSID());
//做漫游操作還是autoconnect的操作
if (currentConnectedNetwork != null
&& (currentConnectedNetwork.networkId == candidate.networkId
|| currentConnectedNetwork.isLinked(candidate))) {
localLog("connectToNetwork: Roaming from " + currentAssociationId + " to "
+ targetAssociationId);
mStateMachine.autoRoamToNetwork(candidate.networkId, scanResultCandidate);
} else {
localLog("connectToNetwork: Reconnect from " + currentAssociationId + " to "
+ targetAssociationId);
mStateMachine.autoConnectToNetwork(candidate.networkId, scanResultCandidate.BSSID);
}
}
接下來就交給Wifistatemachine去處理了。
/**
* Automatically connect to the network specified
*
* @param networkId ID of the network to connect to
* @param bssid BSSID of the network
*/
public void autoConnectToNetwork(int networkId, String bssid) {
sendMessage(CMD_AUTO_CONNECT, networkId, 0, bssid);
}
/**
* Automatically roam to the network specified
*
* @param networkId ID of the network to roam to
* @param scanResult scan result which identifies the network to roam to
*/
public void autoRoamToNetwork(int networkId, ScanResult scanResult) {
sendMessage(CMD_AUTO_ROAM, networkId, 0, scanResult);
}