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 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 if (isEntering) { 213 // We connected to something! Reset failure counts for everything 214 clearAllFailureCounts(); 215 // If the watchdog trigger was disabled (it triggered), connecting means we did 216 // something right, re-enable it so it can fire again. 217 setWatchdogTriggerEnabled(true); 218 } 219 } 220 221 /** 222 * Increments the failure reason count for the given network, in 'mSsidFailureCount' 223 * Failures are counted per SSID, either; by using the ssid string when the bssid is "any" 224 * or by looking up the ssid attached to a specific bssid 225 * An unused set of counts is also kept which is bssid specific, in 'mRecentAvailableNetworks' 226 * @param ssid of the network that has failed connection 227 * @param bssid of the network that has failed connection, can be "any" 228 * @param reason Message id from WifiStateMachine for this failure 229 */ 230 private void updateFailureCountForNetwork(String ssid, String bssid, int reason) { 231 if (VDBG) { 232 Log.v(TAG, "updateFailureCountForNetwork: [" + ssid + ", " + bssid + ", " 233 + reason + "]"); 234 } 235 if (BSSID_ANY.equals(bssid)) { 236 incrementSsidFailureCount(ssid, reason); 237 } else { 238 // Bssid count is actually unused except for logging purposes 239 // SSID count is incremented within the BSSID counting method 240 incrementBssidFailureCount(ssid, bssid, reason); 241 } 242 } 243 244 /** 245 * Update the per-SSID failure count 246 * @param ssid the ssid to increment failure count for 247 * @param reason the failure type to increment count for 248 */ 249 private void incrementSsidFailureCount(String ssid, int reason) { 250 Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid); 251 if (ssidFails == null) { 252 if (DBG) { 253 Log.v(TAG, "updateFailureCountForNetwork: No networks for ssid = " + ssid); 254 } 255 return; 256 } 257 AvailableNetworkFailureCount failureCount = ssidFails.first; 258 failureCount.incrementFailureCount(reason); 259 } 260 261 /** 262 * Update the per-BSSID failure count 263 * @param bssid the bssid to increment failure count for 264 * @param reason the failure type to increment count for 265 */ 266 private void incrementBssidFailureCount(String ssid, String bssid, int reason) { 267 AvailableNetworkFailureCount availableNetworkFailureCount = 268 mRecentAvailableNetworks.get(bssid); 269 if (availableNetworkFailureCount == null) { 270 if (DBG) { 271 Log.d(TAG, "updateFailureCountForNetwork: Unable to find Network [" + ssid 272 + ", " + bssid + "]"); 273 } 274 return; 275 } 276 if (!availableNetworkFailureCount.ssid.equals(ssid)) { 277 if (DBG) { 278 Log.d(TAG, "updateFailureCountForNetwork: Failed connection attempt has" 279 + " wrong ssid. Failed [" + ssid + ", " + bssid + "], buffered [" 280 + availableNetworkFailureCount.ssid + ", " + bssid + "]"); 281 } 282 return; 283 } 284 if (availableNetworkFailureCount.config == null) { 285 if (VDBG) { 286 Log.v(TAG, "updateFailureCountForNetwork: network has no config [" 287 + ssid + ", " + bssid + "]"); 288 } 289 } 290 availableNetworkFailureCount.incrementFailureCount(reason); 291 incrementSsidFailureCount(ssid, reason); 292 } 293 294 /** 295 * Check trigger condition: For all available networks, have we met a failure threshold for each 296 * of them, and have previously connected to at-least one of the available networks 297 * @return is the trigger condition true 298 */ 299 private boolean checkTriggerCondition() { 300 if (VDBG) Log.v(TAG, "checkTriggerCondition."); 301 // Don't check Watchdog trigger if wifi is in a connected state 302 // (This should not occur, but we want to protect against any race conditions) 303 if (mWifiIsConnected) return false; 304 // Don't check Watchdog trigger if trigger is not enabled 305 if (!mWatchdogAllowedToTrigger) return false; 306 307 boolean atleastOneNetworkHasEverConnected = false; 308 for (Map.Entry<String, AvailableNetworkFailureCount> entry 309 : mRecentAvailableNetworks.entrySet()) { 310 if (entry.getValue().config != null 311 && entry.getValue().config.getNetworkSelectionStatus().getHasEverConnected()) { 312 atleastOneNetworkHasEverConnected = true; 313 } 314 if (!isOverFailureThreshold(entry.getKey())) { 315 // This available network is not over failure threshold, meaning we still have a 316 // network to try connecting to 317 return false; 318 } 319 } 320 // We have met the failure count for every available network & there is at-least one network 321 // we have previously connected to present. 322 if (VDBG) { 323 Log.v(TAG, "checkTriggerCondition: return = " + atleastOneNetworkHasEverConnected); 324 } 325 return atleastOneNetworkHasEverConnected; 326 } 327 328 /** 329 * Trigger a restart of the wifi stack. 330 */ 331 private void restartWifiStack() { 332 if (VDBG) Log.v(TAG, "restartWifiStack."); 333 334 // First verify that we can send the trigger message. 335 if (mWifiController == null) { 336 Log.e(TAG, "WifiLastResortWatchdog unable to trigger: WifiController is null"); 337 return; 338 } 339 340 if (DBG) Log.d(TAG, toString()); 341 342 mWifiController.sendMessage(WifiController.CMD_RESTART_WIFI); 343 Log.i(TAG, "Triggered WiFi stack restart."); 344 } 345 346 /** 347 * Update WifiMetrics with various Watchdog stats (trigger counts, failed network counts) 348 */ 349 private void incrementWifiMetricsTriggerCounts() { 350 if (VDBG) Log.v(TAG, "incrementWifiMetricsTriggerCounts."); 351 mWifiMetrics.incrementNumLastResortWatchdogTriggers(); 352 mWifiMetrics.addCountToNumLastResortWatchdogAvailableNetworksTotal( 353 mSsidFailureCount.size()); 354 // Number of networks over each failure type threshold, present at trigger time 355 int badAuth = 0; 356 int badAssoc = 0; 357 int badDhcp = 0; 358 for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry 359 : mSsidFailureCount.entrySet()) { 360 badAuth += (entry.getValue().first.authenticationFailure >= FAILURE_THRESHOLD) ? 1 : 0; 361 badAssoc += (entry.getValue().first.associationRejection >= FAILURE_THRESHOLD) ? 1 : 0; 362 badDhcp += (entry.getValue().first.dhcpFailure >= FAILURE_THRESHOLD) ? 1 : 0; 363 } 364 if (badAuth > 0) { 365 mWifiMetrics.addCountToNumLastResortWatchdogBadAuthenticationNetworksTotal(badAuth); 366 mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAuthentication(); 367 } 368 if (badAssoc > 0) { 369 mWifiMetrics.addCountToNumLastResortWatchdogBadAssociationNetworksTotal(badAssoc); 370 mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAssociation(); 371 } 372 if (badDhcp > 0) { 373 mWifiMetrics.addCountToNumLastResortWatchdogBadDhcpNetworksTotal(badDhcp); 374 mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadDhcp(); 375 } 376 } 377 378 /** 379 * Clear failure counts for each network in recentAvailableNetworks 380 */ 381 private void clearAllFailureCounts() { 382 if (VDBG) Log.v(TAG, "clearAllFailureCounts."); 383 for (Map.Entry<String, AvailableNetworkFailureCount> entry 384 : mRecentAvailableNetworks.entrySet()) { 385 final AvailableNetworkFailureCount failureCount = entry.getValue(); 386 entry.getValue().resetCounts(); 387 } 388 for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry 389 : mSsidFailureCount.entrySet()) { 390 final AvailableNetworkFailureCount failureCount = entry.getValue().first; 391 failureCount.resetCounts(); 392 } 393 } 394 /** 395 * Gets the buffer of recently available networks 396 */ 397 Map<String, AvailableNetworkFailureCount> getRecentAvailableNetworks() { 398 return mRecentAvailableNetworks; 399 } 400 401 /** 402 * Activates or deactivates the Watchdog trigger. Counting and network buffering still occurs 403 * @param enable true to enable the Watchdog trigger, false to disable it 404 */ 405 private void setWatchdogTriggerEnabled(boolean enable) { 406 if (VDBG) Log.v(TAG, "setWatchdogTriggerEnabled: enable = " + enable); 407 mWatchdogAllowedToTrigger = enable; 408 } 409 410 /** 411 * Prints all networks & counts within mRecentAvailableNetworks to string 412 */ 413 public String toString() { 414 StringBuilder sb = new StringBuilder(); 415 sb.append("mWatchdogAllowedToTrigger: ").append(mWatchdogAllowedToTrigger); 416 sb.append("\nmWifiIsConnected: ").append(mWifiIsConnected); 417 sb.append("\nmRecentAvailableNetworks: ").append(mRecentAvailableNetworks.size()); 418 for (Map.Entry<String, AvailableNetworkFailureCount> entry 419 : mRecentAvailableNetworks.entrySet()) { 420 sb.append("\n ").append(entry.getKey()).append(": ").append(entry.getValue()); 421 } 422 sb.append("\nmSsidFailureCount:"); 423 for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry : 424 mSsidFailureCount.entrySet()) { 425 final AvailableNetworkFailureCount failureCount = entry.getValue().first; 426 final Integer apCount = entry.getValue().second; 427 sb.append("\n").append(entry.getKey()).append(": ").append(apCount).append(", ") 428 .append(failureCount.toString()); 429 } 430 return sb.toString(); 431 } 432 433 /** 434 * @param bssid bssid to check the failures for 435 * @return true if any failure count is over FAILURE_THRESHOLD 436 */ 437 public boolean isOverFailureThreshold(String bssid) { 438 if ((getFailureCount(bssid, FAILURE_CODE_ASSOCIATION) >= FAILURE_THRESHOLD) 439 || (getFailureCount(bssid, FAILURE_CODE_AUTHENTICATION) >= FAILURE_THRESHOLD) 440 || (getFailureCount(bssid, FAILURE_CODE_DHCP) >= FAILURE_THRESHOLD)) { 441 return true; 442 } 443 return false; 444 } 445 446 /** 447 * Get the failure count for a specific bssid. This actually checks the ssid attached to the 448 * BSSID and returns the SSID count 449 * @param reason failure reason to get count for 450 */ 451 public int getFailureCount(String bssid, int reason) { 452 AvailableNetworkFailureCount availableNetworkFailureCount = 453 mRecentAvailableNetworks.get(bssid); 454 if (availableNetworkFailureCount == null) { 455 return 0; 456 } 457 String ssid = availableNetworkFailureCount.ssid; 458 Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid); 459 if (ssidFails == null) { 460 if (DBG) { 461 Log.d(TAG, "getFailureCount: Could not find SSID count for " + ssid); 462 } 463 return 0; 464 } 465 final AvailableNetworkFailureCount failCount = ssidFails.first; 466 switch (reason) { 467 case FAILURE_CODE_ASSOCIATION: 468 return failCount.associationRejection; 469 case FAILURE_CODE_AUTHENTICATION: 470 return failCount.authenticationFailure; 471 case FAILURE_CODE_DHCP: 472 return failCount.dhcpFailure; 473 default: 474 return 0; 475 } 476 } 477 478 /** 479 * This class holds the failure counts for an 'available network' (one of the potential 480 * candidates for connection, as determined by framework). 481 */ 482 public static class AvailableNetworkFailureCount { 483 /** 484 * WifiConfiguration associated with this network. Can be null for Ephemeral networks 485 */ 486 public WifiConfiguration config; 487 /** 488 * SSID of the network (from ScanDetail) 489 */ 490 public String ssid = ""; 491 /** 492 * Number of times network has failed due to Association Rejection 493 */ 494 public int associationRejection = 0; 495 /** 496 * Number of times network has failed due to Authentication Failure or SSID_TEMP_DISABLED 497 */ 498 public int authenticationFailure = 0; 499 /** 500 * Number of times network has failed due to DHCP failure 501 */ 502 public int dhcpFailure = 0; 503 /** 504 * Number of scanResults since this network was last seen 505 */ 506 public int age = 0; 507 508 AvailableNetworkFailureCount(WifiConfiguration configParam) { 509 this.config = configParam; 510 } 511 512 /** 513 * @param reason failure reason to increment count for 514 */ 515 public void incrementFailureCount(int reason) { 516 switch (reason) { 517 case FAILURE_CODE_ASSOCIATION: 518 associationRejection++; 519 break; 520 case FAILURE_CODE_AUTHENTICATION: 521 authenticationFailure++; 522 break; 523 case FAILURE_CODE_DHCP: 524 dhcpFailure++; 525 break; 526 default: //do nothing 527 } 528 } 529 530 /** 531 * Set all failure counts for this network to 0 532 */ 533 void resetCounts() { 534 associationRejection = 0; 535 authenticationFailure = 0; 536 dhcpFailure = 0; 537 } 538 539 public String toString() { 540 return ssid + ", HasEverConnected: " + ((config != null) 541 ? config.getNetworkSelectionStatus().getHasEverConnected() : "null_config") 542 + ", Failures: {" 543 + "Assoc: " + associationRejection 544 + ", Auth: " + authenticationFailure 545 + ", Dhcp: " + dhcpFailure 546 + "}" 547 + ", Age: " + age; 548 } 549 } 550 551 /** 552 * Method used to set the WifiController for the this watchdog. 553 * 554 * The WifiController is used to send the restart wifi command to carry out the wifi restart. 555 * @param wifiController WifiController instance that will be sent the CMD_RESTART_WIFI message. 556 */ 557 public void setWifiController(WifiController wifiController) { 558 mWifiController = wifiController; 559 } 560 } 561