WifiLastResortWatchdog.java revision 83f2b8087178705445e4d1618eaac832f9c633f4
1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.server.wifi; 18 19import android.net.wifi.ScanResult; 20import android.net.wifi.WifiConfiguration; 21import android.util.Log; 22import android.util.Pair; 23 24import java.util.HashMap; 25import java.util.Iterator; 26import java.util.List; 27import java.util.Map; 28 29/** 30 * This Class is a Work-In-Progress, intended behavior is as follows: 31 * Essentially this class automates a user toggling 'Airplane Mode' when WiFi "won't work". 32 * IF each available saved network has failed connecting more times than the FAILURE_THRESHOLD 33 * THEN Watchdog will restart Supplicant, wifi driver and return WifiStateMachine to InitialState. 34 */ 35public class WifiLastResortWatchdog { 36 private static final String TAG = "WifiLastResortWatchdog"; 37 private static final boolean VDBG = false; 38 private static final boolean DBG = true; 39 /** 40 * Association Failure code 41 */ 42 public static final int FAILURE_CODE_ASSOCIATION = 1; 43 /** 44 * Authentication Failure code 45 */ 46 public static final int FAILURE_CODE_AUTHENTICATION = 2; 47 /** 48 * Dhcp Failure code 49 */ 50 public static final int FAILURE_CODE_DHCP = 3; 51 /** 52 * Maximum number of scan results received since we last saw a BSSID. 53 * If it is not seen before this limit is reached, the network is culled 54 */ 55 public static final int MAX_BSSID_AGE = 10; 56 /** 57 * BSSID used to increment failure counts against ALL bssids associated with a particular SSID 58 */ 59 public static final String BSSID_ANY = "any"; 60 /** 61 * Failure count that each available networks must meet to possibly trigger the Watchdog 62 */ 63 public static final int FAILURE_THRESHOLD = 7; 64 /** 65 * Cached WifiConfigurations of available networks seen within MAX_BSSID_AGE scan results 66 * Key:BSSID, Value:Counters of failure types 67 */ 68 private Map<String, AvailableNetworkFailureCount> mRecentAvailableNetworks = new HashMap<>(); 69 /** 70 * Map of SSID to <FailureCount, AP count>, used to count failures & number of access points 71 * belonging to an SSID. 72 */ 73 private Map<String, Pair<AvailableNetworkFailureCount, Integer>> mSsidFailureCount = 74 new HashMap<>(); 75 // Tracks: if WifiStateMachine is in ConnectedState 76 private boolean mWifiIsConnected = false; 77 // Is Watchdog allowed to trigger now? Set to false after triggering. Set to true after 78 // successfully connecting or a new network (SSID) becomes available to connect to. 79 private boolean mWatchdogAllowedToTrigger = true; 80 81 /** 82 * Refreshes recentAvailableNetworks with the latest available networks 83 * Adds new networks, removes old ones that have timed out. Should be called after Wifi 84 * framework decides what networks it is potentially connecting to. 85 * @param availableNetworks ScanDetail & Config list of potential connection 86 * candidates 87 */ 88 public void updateAvailableNetworks( 89 List<Pair<ScanDetail, WifiConfiguration>> availableNetworks) { 90 if (VDBG) Log.v(TAG, "updateAvailableNetworks: size = " + availableNetworks.size()); 91 // Add new networks to mRecentAvailableNetworks 92 if (availableNetworks != null) { 93 for (Pair<ScanDetail, WifiConfiguration> pair : availableNetworks) { 94 final ScanDetail scanDetail = pair.first; 95 final WifiConfiguration config = pair.second; 96 ScanResult scanResult = scanDetail.getScanResult(); 97 if (scanResult == null) continue; 98 String bssid = scanResult.BSSID; 99 String ssid = "\"" + scanDetail.getSSID() + "\""; 100 if (VDBG) Log.v(TAG, " " + bssid + ": " + scanDetail.getSSID()); 101 // Cache the scanResult & WifiConfig 102 AvailableNetworkFailureCount availableNetworkFailureCount = 103 mRecentAvailableNetworks.get(bssid); 104 if (availableNetworkFailureCount == null) { 105 // New network is available 106 availableNetworkFailureCount = new AvailableNetworkFailureCount(config); 107 availableNetworkFailureCount.ssid = ssid; 108 109 // Count AP for this SSID 110 Pair<AvailableNetworkFailureCount, Integer> ssidFailsAndApCount = 111 mSsidFailureCount.get(ssid); 112 if (ssidFailsAndApCount == null) { 113 // This is a new SSID, create new FailureCount for it and set AP count to 1 114 ssidFailsAndApCount = Pair.create(new AvailableNetworkFailureCount(config), 115 1); 116 setWatchdogTriggerEnabled(true); 117 } else { 118 final Integer numberOfAps = ssidFailsAndApCount.second; 119 // This is not a new SSID, increment the AP count for it 120 ssidFailsAndApCount = Pair.create(ssidFailsAndApCount.first, 121 numberOfAps + 1); 122 } 123 mSsidFailureCount.put(ssid, ssidFailsAndApCount); 124 } 125 // refresh config 126 availableNetworkFailureCount.config = config; 127 // If we saw a network, set its Age to -1 here, aging iteration will set it to 0 128 availableNetworkFailureCount.age = -1; 129 mRecentAvailableNetworks.put(bssid, availableNetworkFailureCount); 130 } 131 } 132 133 // Iterate through available networks updating timeout counts & removing networks. 134 Iterator<Map.Entry<String, AvailableNetworkFailureCount>> it = 135 mRecentAvailableNetworks.entrySet().iterator(); 136 while (it.hasNext()) { 137 Map.Entry<String, AvailableNetworkFailureCount> entry = it.next(); 138 if (entry.getValue().age < MAX_BSSID_AGE - 1) { 139 entry.getValue().age++; 140 } else { 141 // Decrement this SSID : AP count 142 String ssid = entry.getValue().ssid; 143 Pair<AvailableNetworkFailureCount, Integer> ssidFails = 144 mSsidFailureCount.get(ssid); 145 if (ssidFails != null) { 146 Integer apCount = ssidFails.second - 1; 147 if (apCount > 0) { 148 ssidFails = Pair.create(ssidFails.first, apCount); 149 mSsidFailureCount.put(ssid, ssidFails); 150 } else { 151 mSsidFailureCount.remove(ssid); 152 } 153 } else { 154 if (DBG) { 155 Log.d(TAG, "updateAvailableNetworks: SSID to AP count mismatch for " 156 + ssid); 157 } 158 } 159 it.remove(); 160 } 161 } 162 if (VDBG) Log.v(TAG, toString()); 163 } 164 165 /** 166 * Increments the failure reason count for the given bssid. Performs a check to see if we have 167 * exceeded a failure threshold for all available networks, and executes the last resort restart 168 * @param bssid of the network that has failed connection, can be "any" 169 * @param reason Message id from WifiStateMachine for this failure 170 * @return true if watchdog triggers, returned for test visibility 171 */ 172 public boolean noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason) { 173 if (VDBG) { 174 Log.v(TAG, "noteConnectionFailureAndTriggerIfNeeded: [" + ssid + ", " + bssid + ", " 175 + reason + "]"); 176 } 177 // Update failure count for the failing network 178 updateFailureCountForNetwork(ssid, bssid, reason); 179 180 // Have we met conditions to trigger the Watchdog Wifi restart? 181 boolean isRestartNeeded = checkTriggerCondition(); 182 if (isRestartNeeded) { 183 // Stop the watchdog from triggering until re-enabled 184 setWatchdogTriggerEnabled(false); 185 restartWifiStack(); 186 // increment various watchdog trigger count stats 187 incrementWifiMetricsTriggerCounts(); 188 clearAllFailureCounts(); 189 } 190 return isRestartNeeded; 191 } 192 193 /** 194 * Handles transitions entering and exiting WifiStateMachine ConnectedState 195 * Used to track wifistate, and perform watchdog count reseting 196 * @param isEntering true if called from ConnectedState.enter(), false for exit() 197 */ 198 public void connectedStateTransition(boolean isEntering) { 199 if (VDBG) Log.v(TAG, "connectedStateTransition: isEntering = " + isEntering); 200 mWifiIsConnected = isEntering; 201 if (isEntering) { 202 // We connected to something! Reset failure counts for everything 203 clearAllFailureCounts(); 204 // If the watchdog trigger was disabled (it triggered), connecting means we did 205 // something right, re-enable it so it can fire again. 206 setWatchdogTriggerEnabled(true); 207 } 208 } 209 210 /** 211 * Increments the failure reason count for the given network, in 'mSsidFailureCount' 212 * Failures are counted per SSID, either; by using the ssid string when the bssid is "any" 213 * or by looking up the ssid attached to a specific bssid 214 * An unused set of counts is also kept which is bssid specific, in 'mRecentAvailableNetworks' 215 * @param ssid of the network that has failed connection 216 * @param bssid of the network that has failed connection, can be "any" 217 * @param reason Message id from WifiStateMachine for this failure 218 */ 219 private void updateFailureCountForNetwork(String ssid, String bssid, int reason) { 220 if (VDBG) { 221 Log.v(TAG, "updateFailureCountForNetwork: [" + ssid + ", " + bssid + ", " 222 + reason + "]"); 223 } 224 if (BSSID_ANY.equals(bssid)) { 225 incrementSsidFailureCount(ssid, reason); 226 } else { 227 // Bssid count is actually unused except for logging purposes 228 // SSID count is incremented within the BSSID counting method 229 incrementBssidFailureCount(ssid, bssid, reason); 230 } 231 } 232 233 /** 234 * Update the per-SSID failure count 235 * @param ssid the ssid to increment failure count for 236 * @param reason the failure type to increment count for 237 */ 238 private void incrementSsidFailureCount(String ssid, int reason) { 239 Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid); 240 if (ssidFails == null) { 241 if (DBG) { 242 Log.v(TAG, "updateFailureCountForNetwork: No networks for ssid = " + ssid); 243 } 244 return; 245 } 246 AvailableNetworkFailureCount failureCount = ssidFails.first; 247 failureCount.incrementFailureCount(reason); 248 } 249 250 /** 251 * Update the per-BSSID failure count 252 * @param bssid the bssid to increment failure count for 253 * @param reason the failure type to increment count for 254 */ 255 private void incrementBssidFailureCount(String ssid, String bssid, int reason) { 256 AvailableNetworkFailureCount availableNetworkFailureCount = 257 mRecentAvailableNetworks.get(bssid); 258 if (availableNetworkFailureCount == null) { 259 if (DBG) { 260 Log.d(TAG, "updateFailureCountForNetwork: Unable to find Network [" + ssid 261 + ", " + bssid + "]"); 262 } 263 return; 264 } 265 if (!availableNetworkFailureCount.ssid.equals(ssid)) { 266 if (DBG) { 267 Log.d(TAG, "updateFailureCountForNetwork: Failed connection attempt has" 268 + " wrong ssid. Failed [" + ssid + ", " + bssid + "], buffered [" 269 + availableNetworkFailureCount.ssid + ", " + bssid + "]"); 270 } 271 return; 272 } 273 if (availableNetworkFailureCount.config == null) { 274 if (VDBG) { 275 Log.v(TAG, "updateFailureCountForNetwork: network has no config [" 276 + ssid + ", " + bssid + "]"); 277 } 278 } 279 availableNetworkFailureCount.incrementFailureCount(reason); 280 incrementSsidFailureCount(ssid, reason); 281 } 282 283 /** 284 * Check trigger condition: For all available networks, have we met a failure threshold for each 285 * of them, and have previously connected to at-least one of the available networks 286 * @return is the trigger condition true 287 */ 288 private boolean checkTriggerCondition() { 289 if (VDBG) Log.v(TAG, "checkTriggerCondition:"); 290 // Don't check Watchdog trigger if wifi is in a connected state 291 // (This should not occur, but we want to protect against any race conditions) 292 if (mWifiIsConnected) return false; 293 // Don't check Watchdog trigger if trigger is not enabled 294 if (!mWatchdogAllowedToTrigger) return false; 295 296 boolean atleastOneNetworkHasEverConnected = false; 297 for (Map.Entry<String, AvailableNetworkFailureCount> entry 298 : mRecentAvailableNetworks.entrySet()) { 299 if (entry.getValue().config != null 300 && entry.getValue().config.getNetworkSelectionStatus().getHasEverConnected()) { 301 atleastOneNetworkHasEverConnected = true; 302 } 303 if (!isOverFailureThreshold(entry.getKey())) { 304 // This available network is not over failure threshold, meaning we still have a 305 // network to try connecting to 306 return false; 307 } 308 } 309 // We have met the failure count for every available network & there is at-least one network 310 // we have previously connected to present. 311 if (VDBG) { 312 Log.v(TAG, "checkTriggerCondition: return = " + atleastOneNetworkHasEverConnected); 313 } 314 return atleastOneNetworkHasEverConnected; 315 } 316 317 /** 318 * Restart Supplicant, Driver & return WifiStateMachine to InitialState 319 */ 320 private void restartWifiStack() { 321 if (VDBG) Log.v(TAG, "restartWifiStack."); 322 Log.i(TAG, "Triggered."); 323 if (DBG) Log.d(TAG, toString()); 324 // <TODO> 325 } 326 327 /** 328 * Update WifiMetrics with various Watchdog stats (trigger counts, tracked network count) 329 */ 330 private void incrementWifiMetricsTriggerCounts() { 331 if (VDBG) Log.v(TAG, "incrementWifiMetricsTriggerCounts."); 332 // <TODO> 333 } 334 335 /** 336 * Clear failure counts for each network in recentAvailableNetworks 337 */ 338 private void clearAllFailureCounts() { 339 if (VDBG) Log.v(TAG, "clearAllFailureCounts."); 340 for (Map.Entry<String, AvailableNetworkFailureCount> entry 341 : mRecentAvailableNetworks.entrySet()) { 342 final AvailableNetworkFailureCount failureCount = entry.getValue(); 343 entry.getValue().resetCounts(); 344 } 345 for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry 346 : mSsidFailureCount.entrySet()) { 347 final AvailableNetworkFailureCount failureCount = entry.getValue().first; 348 failureCount.resetCounts(); 349 } 350 } 351 /** 352 * Gets the buffer of recently available networks 353 */ 354 Map<String, AvailableNetworkFailureCount> getRecentAvailableNetworks() { 355 return mRecentAvailableNetworks; 356 } 357 358 /** 359 * Activates or deactivates the Watchdog trigger. Counting and network buffering still occurs 360 * @param enable true to enable the Watchdog trigger, false to disable it 361 */ 362 private void setWatchdogTriggerEnabled(boolean enable) { 363 if (VDBG) Log.v(TAG, "setWatchdogTriggerEnabled: enable = " + enable); 364 mWatchdogAllowedToTrigger = enable; 365 } 366 367 /** 368 * Prints all networks & counts within mRecentAvailableNetworks to string 369 */ 370 public String toString() { 371 StringBuilder sb = new StringBuilder(); 372 sb.append("mWatchdogAllowedToTrigger: ").append(mWatchdogAllowedToTrigger); 373 sb.append("\nmWifiIsConnected: ").append(mWifiIsConnected); 374 sb.append("\nmRecentAvailableNetworks: ").append(mRecentAvailableNetworks.size()); 375 for (Map.Entry<String, AvailableNetworkFailureCount> entry 376 : mRecentAvailableNetworks.entrySet()) { 377 sb.append("\n ").append(entry.getKey()).append(": ").append(entry.getValue()); 378 } 379 sb.append("\nmSsidFailureCount:"); 380 for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry : 381 mSsidFailureCount.entrySet()) { 382 final AvailableNetworkFailureCount failureCount = entry.getValue().first; 383 final Integer apCount = entry.getValue().second; 384 sb.append("\n").append(entry.getKey()).append(": ").append(apCount).append(", ") 385 .append(failureCount.toString()); 386 } 387 return sb.toString(); 388 } 389 390 /** 391 * @param bssid bssid to check the failures for 392 * @return true if any failure count is over FAILURE_THRESHOLD 393 */ 394 public boolean isOverFailureThreshold(String bssid) { 395 if ((getFailureCount(bssid, FAILURE_CODE_ASSOCIATION) >= FAILURE_THRESHOLD) 396 || (getFailureCount(bssid, FAILURE_CODE_AUTHENTICATION) >= FAILURE_THRESHOLD) 397 || (getFailureCount(bssid, FAILURE_CODE_DHCP) >= FAILURE_THRESHOLD)) { 398 return true; 399 } 400 return false; 401 } 402 403 /** 404 * Get the failure count for a specific bssid. This actually checks the ssid attached to the 405 * BSSID and returns the SSID count 406 * @param reason failure reason to get count for 407 */ 408 public int getFailureCount(String bssid, int reason) { 409 AvailableNetworkFailureCount availableNetworkFailureCount = 410 mRecentAvailableNetworks.get(bssid); 411 if (availableNetworkFailureCount == null) { 412 return 0; 413 } 414 String ssid = availableNetworkFailureCount.ssid; 415 Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid); 416 if (ssidFails == null) { 417 if (DBG) { 418 Log.d(TAG, "getFailureCount: Could not find SSID count for " + ssid); 419 } 420 return 0; 421 } 422 final AvailableNetworkFailureCount failCount = ssidFails.first; 423 switch (reason) { 424 case FAILURE_CODE_ASSOCIATION: 425 return failCount.associationRejection; 426 case FAILURE_CODE_AUTHENTICATION: 427 return failCount.authenticationFailure; 428 case FAILURE_CODE_DHCP: 429 return failCount.dhcpFailure; 430 default: 431 return 0; 432 } 433 } 434 435 /** 436 * This class holds the failure counts for an 'available network' (one of the potential 437 * candidates for connection, as determined by framework). 438 */ 439 public static class AvailableNetworkFailureCount { 440 /** 441 * WifiConfiguration associated with this network. Can be null for Ephemeral networks 442 */ 443 public WifiConfiguration config; 444 /** 445 * SSID of the network (from ScanDetail) 446 */ 447 public String ssid = ""; 448 /** 449 * Number of times network has failed due to Association Rejection 450 */ 451 public int associationRejection = 0; 452 /** 453 * Number of times network has failed due to Authentication Failure or SSID_TEMP_DISABLED 454 */ 455 public int authenticationFailure = 0; 456 /** 457 * Number of times network has failed due to DHCP failure 458 */ 459 public int dhcpFailure = 0; 460 /** 461 * Number of scanResults since this network was last seen 462 */ 463 public int age = 0; 464 465 AvailableNetworkFailureCount(WifiConfiguration config) { 466 config = config; 467 } 468 469 /** 470 * @param reason failure reason to increment count for 471 */ 472 public void incrementFailureCount(int reason) { 473 switch (reason) { 474 case FAILURE_CODE_ASSOCIATION: 475 associationRejection++; 476 break; 477 case FAILURE_CODE_AUTHENTICATION: 478 authenticationFailure++; 479 break; 480 case FAILURE_CODE_DHCP: 481 dhcpFailure++; 482 break; 483 default: //do nothing 484 } 485 } 486 487 /** 488 * Set all failure counts for this network to 0 489 */ 490 void resetCounts() { 491 associationRejection = 0; 492 authenticationFailure = 0; 493 dhcpFailure = 0; 494 } 495 496 public String toString() { 497 return ssid + ", HasEverConnected: " + ((config != null) 498 ? config.getNetworkSelectionStatus().getHasEverConnected() : false) 499 + ", Failures: {" 500 + "Assoc: " + associationRejection 501 + ", Auth: " + authenticationFailure 502 + ", Dhcp: " + dhcpFailure 503 + "}" 504 + ", Age: " + age; 505 } 506 } 507} 508