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