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 private WifiMetrics mWifiMetrics; 82 83 private WifiController mWifiController = null; 84 85 WifiLastResortWatchdog(WifiMetrics wifiMetrics) { 86 mWifiMetrics = wifiMetrics; 87 } 88 89 /** 90 * Refreshes recentAvailableNetworks with the latest available networks 91 * Adds new networks, removes old ones that have timed out. Should be called after Wifi 92 * framework decides what networks it is potentially connecting to. 93 * @param availableNetworks ScanDetail & Config list of potential connection 94 * candidates 95 */ 96 public void updateAvailableNetworks( 97 List<Pair<ScanDetail, WifiConfiguration>> availableNetworks) { 98 if (VDBG) Log.v(TAG, "updateAvailableNetworks: size = " + availableNetworks.size()); 99 // Add new networks to mRecentAvailableNetworks 100 if (availableNetworks != null) { 101 for (Pair<ScanDetail, WifiConfiguration> pair : availableNetworks) { 102 final ScanDetail scanDetail = pair.first; 103 final WifiConfiguration config = pair.second; 104 ScanResult scanResult = scanDetail.getScanResult(); 105 if (scanResult == null) continue; 106 String bssid = scanResult.BSSID; 107 String ssid = "\"" + scanDetail.getSSID() + "\""; 108 if (VDBG) Log.v(TAG, " " + bssid + ": " + scanDetail.getSSID()); 109 // Cache the scanResult & WifiConfig 110 AvailableNetworkFailureCount availableNetworkFailureCount = 111 mRecentAvailableNetworks.get(bssid); 112 if (availableNetworkFailureCount == null) { 113 // New network is available 114 availableNetworkFailureCount = new AvailableNetworkFailureCount(config); 115 availableNetworkFailureCount.ssid = ssid; 116 117 // Count AP for this SSID 118 Pair<AvailableNetworkFailureCount, Integer> ssidFailsAndApCount = 119 mSsidFailureCount.get(ssid); 120 if (ssidFailsAndApCount == null) { 121 // This is a new SSID, create new FailureCount for it and set AP count to 1 122 ssidFailsAndApCount = Pair.create(new AvailableNetworkFailureCount(config), 123 1); 124 setWatchdogTriggerEnabled(true); 125 } else { 126 final Integer numberOfAps = ssidFailsAndApCount.second; 127 // This is not a new SSID, increment the AP count for it 128 ssidFailsAndApCount = Pair.create(ssidFailsAndApCount.first, 129 numberOfAps + 1); 130 } 131 mSsidFailureCount.put(ssid, ssidFailsAndApCount); 132 } 133 // refresh config if it is not null 134 if (config != null) { 135 availableNetworkFailureCount.config = config; 136 } 137 // If we saw a network, set its Age to -1 here, aging iteration will set it to 0 138 availableNetworkFailureCount.age = -1; 139 mRecentAvailableNetworks.put(bssid, availableNetworkFailureCount); 140 } 141 } 142 143 // Iterate through available networks updating timeout counts & removing networks. 144 Iterator<Map.Entry<String, AvailableNetworkFailureCount>> it = 145 mRecentAvailableNetworks.entrySet().iterator(); 146 while (it.hasNext()) { 147 Map.Entry<String, AvailableNetworkFailureCount> entry = it.next(); 148 if (entry.getValue().age < MAX_BSSID_AGE - 1) { 149 entry.getValue().age++; 150 } else { 151 // Decrement this SSID : AP count 152 String ssid = entry.getValue().ssid; 153 Pair<AvailableNetworkFailureCount, Integer> ssidFails = 154 mSsidFailureCount.get(ssid); 155 if (ssidFails != null) { 156 Integer apCount = ssidFails.second - 1; 157 if (apCount > 0) { 158 ssidFails = Pair.create(ssidFails.first, apCount); 159 mSsidFailureCount.put(ssid, ssidFails); 160 } else { 161 mSsidFailureCount.remove(ssid); 162 } 163 } else { 164 if (DBG) { 165 Log.d(TAG, "updateAvailableNetworks: SSID to AP count mismatch for " 166 + ssid); 167 } 168 } 169 it.remove(); 170 } 171 } 172 if (VDBG) Log.v(TAG, toString()); 173 } 174 175 /** 176 * Increments the failure reason count for the given bssid. Performs a check to see if we have 177 * exceeded a failure threshold for all available networks, and executes the last resort restart 178 * @param bssid of the network that has failed connection, can be "any" 179 * @param reason Message id from WifiStateMachine for this failure 180 * @return true if watchdog triggers, returned for test visibility 181 */ 182 public boolean noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason) { 183 if (VDBG) { 184 Log.v(TAG, "noteConnectionFailureAndTriggerIfNeeded: [" + ssid + ", " + bssid + ", " 185 + reason + "]"); 186 } 187 // Update failure count for the failing network 188 updateFailureCountForNetwork(ssid, bssid, reason); 189 190 // Have we met conditions to trigger the Watchdog Wifi restart? 191 boolean isRestartNeeded = checkTriggerCondition(); 192 if (VDBG) Log.v(TAG, "isRestartNeeded = " + isRestartNeeded); 193 if (isRestartNeeded) { 194 // Stop the watchdog from triggering until re-enabled 195 setWatchdogTriggerEnabled(false); 196 restartWifiStack(); 197 // increment various watchdog trigger count stats 198 incrementWifiMetricsTriggerCounts(); 199 clearAllFailureCounts(); 200 } 201 return isRestartNeeded; 202 } 203 204 /** 205 * Handles transitions entering and exiting WifiStateMachine ConnectedState 206 * Used to track wifistate, and perform watchdog count reseting 207 * @param isEntering true if called from ConnectedState.enter(), false for exit() 208 */ 209 public void connectedStateTransition(boolean isEntering) { 210 if (VDBG) Log.v(TAG, "connectedStateTransition: isEntering = " + isEntering); 211 mWifiIsConnected = isEntering; 212 213 if (!mWatchdogAllowedToTrigger) { 214 // WiFi has connected after a Watchdog trigger, without any new networks becoming 215 // available, log a Watchdog success in wifi metrics 216 mWifiMetrics.incrementNumLastResortWatchdogSuccesses(); 217 } 218 if (isEntering) { 219 // We connected to something! Reset failure counts for everything 220 clearAllFailureCounts(); 221 // If the watchdog trigger was disabled (it triggered), connecting means we did 222 // something right, re-enable it so it can fire again. 223 setWatchdogTriggerEnabled(true); 224 } 225 } 226 227 /** 228 * Increments the failure reason count for the given network, in 'mSsidFailureCount' 229 * Failures are counted per SSID, either; by using the ssid string when the bssid is "any" 230 * or by looking up the ssid attached to a specific bssid 231 * An unused set of counts is also kept which is bssid specific, in 'mRecentAvailableNetworks' 232 * @param ssid of the network that has failed connection 233 * @param bssid of the network that has failed connection, can be "any" 234 * @param reason Message id from WifiStateMachine for this failure 235 */ 236 private void updateFailureCountForNetwork(String ssid, String bssid, int reason) { 237 if (VDBG) { 238 Log.v(TAG, "updateFailureCountForNetwork: [" + ssid + ", " + bssid + ", " 239 + reason + "]"); 240 } 241 if (BSSID_ANY.equals(bssid)) { 242 incrementSsidFailureCount(ssid, reason); 243 } else { 244 // Bssid count is actually unused except for logging purposes 245 // SSID count is incremented within the BSSID counting method 246 incrementBssidFailureCount(ssid, bssid, reason); 247 } 248 } 249 250 /** 251 * Update the per-SSID failure count 252 * @param ssid the ssid to increment failure count for 253 * @param reason the failure type to increment count for 254 */ 255 private void incrementSsidFailureCount(String ssid, int reason) { 256 Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid); 257 if (ssidFails == null) { 258 if (DBG) { 259 Log.v(TAG, "updateFailureCountForNetwork: No networks for ssid = " + ssid); 260 } 261 return; 262 } 263 AvailableNetworkFailureCount failureCount = ssidFails.first; 264 failureCount.incrementFailureCount(reason); 265 } 266 267 /** 268 * Update the per-BSSID failure count 269 * @param bssid the bssid to increment failure count for 270 * @param reason the failure type to increment count for 271 */ 272 private void incrementBssidFailureCount(String ssid, String bssid, int reason) { 273 AvailableNetworkFailureCount availableNetworkFailureCount = 274 mRecentAvailableNetworks.get(bssid); 275 if (availableNetworkFailureCount == null) { 276 if (DBG) { 277 Log.d(TAG, "updateFailureCountForNetwork: Unable to find Network [" + ssid 278 + ", " + bssid + "]"); 279 } 280 return; 281 } 282 if (!availableNetworkFailureCount.ssid.equals(ssid)) { 283 if (DBG) { 284 Log.d(TAG, "updateFailureCountForNetwork: Failed connection attempt has" 285 + " wrong ssid. Failed [" + ssid + ", " + bssid + "], buffered [" 286 + availableNetworkFailureCount.ssid + ", " + bssid + "]"); 287 } 288 return; 289 } 290 if (availableNetworkFailureCount.config == null) { 291 if (VDBG) { 292 Log.v(TAG, "updateFailureCountForNetwork: network has no config [" 293 + ssid + ", " + bssid + "]"); 294 } 295 } 296 availableNetworkFailureCount.incrementFailureCount(reason); 297 incrementSsidFailureCount(ssid, reason); 298 } 299 300 /** 301 * Check trigger condition: For all available networks, have we met a failure threshold for each 302 * of them, and have previously connected to at-least one of the available networks 303 * @return is the trigger condition true 304 */ 305 private boolean checkTriggerCondition() { 306 if (VDBG) Log.v(TAG, "checkTriggerCondition."); 307 // Don't check Watchdog trigger if wifi is in a connected state 308 // (This should not occur, but we want to protect against any race conditions) 309 if (mWifiIsConnected) return false; 310 // Don't check Watchdog trigger if trigger is not enabled 311 if (!mWatchdogAllowedToTrigger) return false; 312 313 boolean atleastOneNetworkHasEverConnected = false; 314 for (Map.Entry<String, AvailableNetworkFailureCount> entry 315 : mRecentAvailableNetworks.entrySet()) { 316 if (entry.getValue().config != null 317 && entry.getValue().config.getNetworkSelectionStatus().getHasEverConnected()) { 318 atleastOneNetworkHasEverConnected = true; 319 } 320 if (!isOverFailureThreshold(entry.getKey())) { 321 // This available network is not over failure threshold, meaning we still have a 322 // network to try connecting to 323 return false; 324 } 325 } 326 // We have met the failure count for every available network & there is at-least one network 327 // we have previously connected to present. 328 if (VDBG) { 329 Log.v(TAG, "checkTriggerCondition: return = " + atleastOneNetworkHasEverConnected); 330 } 331 return atleastOneNetworkHasEverConnected; 332 } 333 334 /** 335 * Trigger a restart of the wifi stack. 336 */ 337 private void restartWifiStack() { 338 if (VDBG) Log.v(TAG, "restartWifiStack."); 339 340 // First verify that we can send the trigger message. 341 if (mWifiController == null) { 342 Log.e(TAG, "WifiLastResortWatchdog unable to trigger: WifiController is null"); 343 return; 344 } 345 346 if (DBG) Log.d(TAG, toString()); 347 348 mWifiController.sendMessage(WifiController.CMD_RESTART_WIFI); 349 Log.i(TAG, "Triggered WiFi stack restart."); 350 } 351 352 /** 353 * Update WifiMetrics with various Watchdog stats (trigger counts, failed network counts) 354 */ 355 private void incrementWifiMetricsTriggerCounts() { 356 if (VDBG) Log.v(TAG, "incrementWifiMetricsTriggerCounts."); 357 mWifiMetrics.incrementNumLastResortWatchdogTriggers(); 358 mWifiMetrics.addCountToNumLastResortWatchdogAvailableNetworksTotal( 359 mSsidFailureCount.size()); 360 // Number of networks over each failure type threshold, present at trigger time 361 int badAuth = 0; 362 int badAssoc = 0; 363 int badDhcp = 0; 364 for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry 365 : mSsidFailureCount.entrySet()) { 366 badAuth += (entry.getValue().first.authenticationFailure >= FAILURE_THRESHOLD) ? 1 : 0; 367 badAssoc += (entry.getValue().first.associationRejection >= FAILURE_THRESHOLD) ? 1 : 0; 368 badDhcp += (entry.getValue().first.dhcpFailure >= FAILURE_THRESHOLD) ? 1 : 0; 369 } 370 if (badAuth > 0) { 371 mWifiMetrics.addCountToNumLastResortWatchdogBadAuthenticationNetworksTotal(badAuth); 372 mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAuthentication(); 373 } 374 if (badAssoc > 0) { 375 mWifiMetrics.addCountToNumLastResortWatchdogBadAssociationNetworksTotal(badAssoc); 376 mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAssociation(); 377 } 378 if (badDhcp > 0) { 379 mWifiMetrics.addCountToNumLastResortWatchdogBadDhcpNetworksTotal(badDhcp); 380 mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadDhcp(); 381 } 382 } 383 384 /** 385 * Clear failure counts for each network in recentAvailableNetworks 386 */ 387 private void clearAllFailureCounts() { 388 if (VDBG) Log.v(TAG, "clearAllFailureCounts."); 389 for (Map.Entry<String, AvailableNetworkFailureCount> entry 390 : mRecentAvailableNetworks.entrySet()) { 391 final AvailableNetworkFailureCount failureCount = entry.getValue(); 392 entry.getValue().resetCounts(); 393 } 394 for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry 395 : mSsidFailureCount.entrySet()) { 396 final AvailableNetworkFailureCount failureCount = entry.getValue().first; 397 failureCount.resetCounts(); 398 } 399 } 400 /** 401 * Gets the buffer of recently available networks 402 */ 403 Map<String, AvailableNetworkFailureCount> getRecentAvailableNetworks() { 404 return mRecentAvailableNetworks; 405 } 406 407 /** 408 * Activates or deactivates the Watchdog trigger. Counting and network buffering still occurs 409 * @param enable true to enable the Watchdog trigger, false to disable it 410 */ 411 private void setWatchdogTriggerEnabled(boolean enable) { 412 if (VDBG) Log.v(TAG, "setWatchdogTriggerEnabled: enable = " + enable); 413 mWatchdogAllowedToTrigger = enable; 414 } 415 416 /** 417 * Prints all networks & counts within mRecentAvailableNetworks to string 418 */ 419 public String toString() { 420 StringBuilder sb = new StringBuilder(); 421 sb.append("mWatchdogAllowedToTrigger: ").append(mWatchdogAllowedToTrigger); 422 sb.append("\nmWifiIsConnected: ").append(mWifiIsConnected); 423 sb.append("\nmRecentAvailableNetworks: ").append(mRecentAvailableNetworks.size()); 424 for (Map.Entry<String, AvailableNetworkFailureCount> entry 425 : mRecentAvailableNetworks.entrySet()) { 426 sb.append("\n ").append(entry.getKey()).append(": ").append(entry.getValue()); 427 } 428 sb.append("\nmSsidFailureCount:"); 429 for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry : 430 mSsidFailureCount.entrySet()) { 431 final AvailableNetworkFailureCount failureCount = entry.getValue().first; 432 final Integer apCount = entry.getValue().second; 433 sb.append("\n").append(entry.getKey()).append(": ").append(apCount).append(", ") 434 .append(failureCount.toString()); 435 } 436 return sb.toString(); 437 } 438 439 /** 440 * @param bssid bssid to check the failures for 441 * @return true if any failure count is over FAILURE_THRESHOLD 442 */ 443 public boolean isOverFailureThreshold(String bssid) { 444 if ((getFailureCount(bssid, FAILURE_CODE_ASSOCIATION) >= FAILURE_THRESHOLD) 445 || (getFailureCount(bssid, FAILURE_CODE_AUTHENTICATION) >= FAILURE_THRESHOLD) 446 || (getFailureCount(bssid, FAILURE_CODE_DHCP) >= FAILURE_THRESHOLD)) { 447 return true; 448 } 449 return false; 450 } 451 452 /** 453 * Get the failure count for a specific bssid. This actually checks the ssid attached to the 454 * BSSID and returns the SSID count 455 * @param reason failure reason to get count for 456 */ 457 public int getFailureCount(String bssid, int reason) { 458 AvailableNetworkFailureCount availableNetworkFailureCount = 459 mRecentAvailableNetworks.get(bssid); 460 if (availableNetworkFailureCount == null) { 461 return 0; 462 } 463 String ssid = availableNetworkFailureCount.ssid; 464 Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid); 465 if (ssidFails == null) { 466 if (DBG) { 467 Log.d(TAG, "getFailureCount: Could not find SSID count for " + ssid); 468 } 469 return 0; 470 } 471 final AvailableNetworkFailureCount failCount = ssidFails.first; 472 switch (reason) { 473 case FAILURE_CODE_ASSOCIATION: 474 return failCount.associationRejection; 475 case FAILURE_CODE_AUTHENTICATION: 476 return failCount.authenticationFailure; 477 case FAILURE_CODE_DHCP: 478 return failCount.dhcpFailure; 479 default: 480 return 0; 481 } 482 } 483 484 /** 485 * This class holds the failure counts for an 'available network' (one of the potential 486 * candidates for connection, as determined by framework). 487 */ 488 public static class AvailableNetworkFailureCount { 489 /** 490 * WifiConfiguration associated with this network. Can be null for Ephemeral networks 491 */ 492 public WifiConfiguration config; 493 /** 494 * SSID of the network (from ScanDetail) 495 */ 496 public String ssid = ""; 497 /** 498 * Number of times network has failed due to Association Rejection 499 */ 500 public int associationRejection = 0; 501 /** 502 * Number of times network has failed due to Authentication Failure or SSID_TEMP_DISABLED 503 */ 504 public int authenticationFailure = 0; 505 /** 506 * Number of times network has failed due to DHCP failure 507 */ 508 public int dhcpFailure = 0; 509 /** 510 * Number of scanResults since this network was last seen 511 */ 512 public int age = 0; 513 514 AvailableNetworkFailureCount(WifiConfiguration configParam) { 515 this.config = configParam; 516 } 517 518 /** 519 * @param reason failure reason to increment count for 520 */ 521 public void incrementFailureCount(int reason) { 522 switch (reason) { 523 case FAILURE_CODE_ASSOCIATION: 524 associationRejection++; 525 break; 526 case FAILURE_CODE_AUTHENTICATION: 527 authenticationFailure++; 528 break; 529 case FAILURE_CODE_DHCP: 530 dhcpFailure++; 531 break; 532 default: //do nothing 533 } 534 } 535 536 /** 537 * Set all failure counts for this network to 0 538 */ 539 void resetCounts() { 540 associationRejection = 0; 541 authenticationFailure = 0; 542 dhcpFailure = 0; 543 } 544 545 public String toString() { 546 return ssid + ", HasEverConnected: " + ((config != null) 547 ? config.getNetworkSelectionStatus().getHasEverConnected() : "null_config") 548 + ", Failures: {" 549 + "Assoc: " + associationRejection 550 + ", Auth: " + authenticationFailure 551 + ", Dhcp: " + dhcpFailure 552 + "}" 553 + ", Age: " + age; 554 } 555 } 556 557 /** 558 * Method used to set the WifiController for the this watchdog. 559 * 560 * The WifiController is used to send the restart wifi command to carry out the wifi restart. 561 * @param wifiController WifiController instance that will be sent the CMD_RESTART_WIFI message. 562 */ 563 public void setWifiController(WifiController wifiController) { 564 mWifiController = wifiController; 565 } 566} 567