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