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.annotation.NonNull; 20 import android.content.Context; 21 import android.net.wifi.ScanResult; 22 import android.net.wifi.WifiConfiguration; 23 import android.net.wifi.WifiInfo; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.os.Message; 27 import android.text.TextUtils; 28 import android.util.LocalLog; 29 import android.util.Log; 30 import android.util.LruCache; 31 import android.util.Pair; 32 33 import com.android.internal.annotations.VisibleForTesting; 34 import com.android.wifi.resources.R; 35 36 import java.io.FileDescriptor; 37 import java.io.PrintWriter; 38 import java.util.HashMap; 39 import java.util.HashSet; 40 import java.util.Iterator; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.Set; 44 45 /** 46 * This Class is a Work-In-Progress, intended behavior is as follows: 47 * Essentially this class automates a user toggling 'Airplane Mode' when WiFi "won't work". 48 * IF each available saved network has failed connecting more times than the FAILURE_THRESHOLD 49 * THEN Watchdog will restart Supplicant, wifi driver and return ClientModeImpl to InitialState. 50 * TODO(b/159944009): May need to rework this class to handle make before break transition on STA + 51 * STA devices. 52 */ 53 public class WifiLastResortWatchdog { 54 private static final String TAG = "WifiLastResortWatchdog"; 55 private boolean mVerboseLoggingEnabled = false; 56 /** 57 * Association Failure code 58 */ 59 public static final int FAILURE_CODE_ASSOCIATION = 1; 60 /** 61 * Authentication Failure code 62 */ 63 public static final int FAILURE_CODE_AUTHENTICATION = 2; 64 /** 65 * Dhcp Failure code 66 */ 67 public static final int FAILURE_CODE_DHCP = 3; 68 /** 69 * Maximum number of scan results received since we last saw a BSSID. 70 * If it is not seen before this limit is reached, the network is culled 71 */ 72 public static final int MAX_BSSID_AGE = 10; 73 /** 74 * BSSID used to increment failure counts against ALL bssids associated with a particular SSID 75 */ 76 public static final String BSSID_ANY = "any"; 77 /** 78 * Failure count that each available networks must meet to possibly trigger the Watchdog 79 */ 80 public static final int FAILURE_THRESHOLD = 7; 81 public static final String BUGREPORT_TITLE = "Wifi watchdog triggered"; 82 public static final double PROB_TAKE_BUGREPORT_DEFAULT = 1; 83 84 // Number of milliseconds to wait before re-enable Watchdog triger 85 @VisibleForTesting 86 public static final long LAST_TRIGGER_TIMEOUT_MILLIS = 2 * 3600 * 1000; // 2 hours 87 88 89 /** 90 * Cached WifiConfigurations of available networks seen within MAX_BSSID_AGE scan results 91 * Key:BSSID, Value:Counters of failure types 92 */ 93 private Map<String, AvailableNetworkFailureCount> mRecentAvailableNetworks = new HashMap<>(); 94 95 /** 96 * Map of SSID to <FailureCount, AP count>, used to count failures & number of access points 97 * belonging to an SSID. 98 */ 99 private Map<String, Pair<AvailableNetworkFailureCount, Integer>> mSsidFailureCount = 100 new HashMap<>(); 101 102 /* List of failure BSSID */ 103 private final Set<String> mBssidFailureList = new HashSet<>(); 104 105 // Is Watchdog allowed to trigger now? Set to false after triggering. Set to true after 106 // successfully connecting or a new network (SSID) becomes available to connect to. 107 private boolean mWatchdogAllowedToTrigger = true; 108 private long mTimeLastTrigger = 0; 109 private String mSsidLastTrigger = null; 110 private double mBugReportProbability = PROB_TAKE_BUGREPORT_DEFAULT; 111 // If any connection failure happened after watchdog triggering restart then assume watchdog 112 // did not fix the problem 113 private boolean mWatchdogFixedWifi = true; 114 /** 115 * int key: networkId 116 * long value: last time we started connecting to this network, in milliseconds since boot 117 * 118 * Limit size to 10 to prevent it from growing without bounds. 119 */ 120 private final LruCache<Integer, Long> mNetworkIdToLastStartConnectTimeMillisSinceBoot = 121 new LruCache<>(10); 122 private Boolean mWatchdogFeatureEnabled = null; 123 124 private final WifiInjector mWifiInjector; 125 private final WifiMetrics mWifiMetrics; 126 private final WifiDiagnostics mWifiDiagnostics; 127 private final Clock mClock; 128 private final Context mContext; 129 private final DeviceConfigFacade mDeviceConfigFacade; 130 private final Handler mHandler; 131 private final WifiThreadRunner mWifiThreadRunner; 132 private final WifiMonitor mWifiMonitor; 133 134 /** 135 * Local log used for debugging any WifiLastResortWatchdog issues. 136 */ 137 private final LocalLog mLocalLog = new LocalLog(100); 138 WifiLastResortWatchdog( WifiInjector wifiInjector, Context context, Clock clock, WifiMetrics wifiMetrics, WifiDiagnostics wifiDiagnostics, Looper clientModeImplLooper, DeviceConfigFacade deviceConfigFacade, WifiThreadRunner wifiThreadRunner, WifiMonitor wifiMonitor)139 WifiLastResortWatchdog( 140 WifiInjector wifiInjector, 141 Context context, Clock clock, 142 WifiMetrics wifiMetrics, 143 WifiDiagnostics wifiDiagnostics, 144 Looper clientModeImplLooper, 145 DeviceConfigFacade deviceConfigFacade, 146 WifiThreadRunner wifiThreadRunner, 147 WifiMonitor wifiMonitor) { 148 mWifiInjector = wifiInjector; 149 mClock = clock; 150 mWifiMetrics = wifiMetrics; 151 mWifiDiagnostics = wifiDiagnostics; 152 mContext = context; 153 mDeviceConfigFacade = deviceConfigFacade; 154 mWifiThreadRunner = wifiThreadRunner; 155 mWifiMonitor = wifiMonitor; 156 mHandler = new Handler(clientModeImplLooper) { 157 public void handleMessage(Message msg) { 158 processMessage(msg); 159 } 160 }; 161 } 162 163 private static final int[] WIFI_MONITOR_EVENTS = { 164 WifiMonitor.NETWORK_CONNECTION_EVENT 165 }; 166 registerForWifiMonitorEvents(String ifaceName)167 public void registerForWifiMonitorEvents(String ifaceName) { 168 for (int event : WIFI_MONITOR_EVENTS) { 169 mWifiMonitor.registerHandler(ifaceName, event, mHandler); 170 } 171 } 172 deregisterForWifiMonitorEvents(String ifaceName)173 public void deregisterForWifiMonitorEvents(String ifaceName) { 174 for (int event : WIFI_MONITOR_EVENTS) { 175 mWifiMonitor.deregisterHandler(ifaceName, event, mHandler); 176 } 177 } 178 179 @NonNull getPrimaryWifiInfo()180 private WifiInfo getPrimaryWifiInfo() { 181 // This is retrieved lazily since there is a non-trivial circular dependency between 182 // ActiveModeWarden & WifiLastResortWatchdog. 183 ActiveModeWarden activeModeWarden = mWifiInjector.getActiveModeWarden(); 184 if (activeModeWarden == null) return new WifiInfo(); 185 // Cannot be null. 186 ClientModeManager primaryCmm = activeModeWarden.getPrimaryClientModeManager(); 187 return primaryCmm.getConnectionInfo(); 188 } 189 190 /** 191 * Refreshes when the last CMD_START_CONNECT is triggered. 192 */ noteStartConnectTime(int networkId)193 public void noteStartConnectTime(int networkId) { 194 mHandler.post(() -> 195 mNetworkIdToLastStartConnectTimeMillisSinceBoot.put( 196 networkId, mClock.getElapsedSinceBootMillis())); 197 } 198 processMessage(Message msg)199 private void processMessage(Message msg) { 200 switch (msg.what) { 201 case WifiMonitor.NETWORK_CONNECTION_EVENT: { 202 NetworkConnectionEventInfo connectionInfo = (NetworkConnectionEventInfo) msg.obj; 203 int networkId = connectionInfo.networkId; 204 // Trigger bugreport for successful connections that take abnormally long 205 Long lastStartConnectTimeNullable = 206 mNetworkIdToLastStartConnectTimeMillisSinceBoot.get(networkId); 207 if (mDeviceConfigFacade.isAbnormalConnectionBugreportEnabled() 208 && lastStartConnectTimeNullable != null) { 209 long durationMs = 210 mClock.getElapsedSinceBootMillis() - lastStartConnectTimeNullable; 211 long abnormalConnectionDurationMs = 212 mDeviceConfigFacade.getAbnormalConnectionDurationMs(); 213 if (durationMs > abnormalConnectionDurationMs) { 214 final String bugTitle = "Wi-Fi Bugreport: Abnormal connection time"; 215 final String bugDetail = "Expected connection to take less than " 216 + abnormalConnectionDurationMs + " milliseconds. " 217 + "Actually took " + durationMs + " milliseconds."; 218 logv("Triggering bug report for abnormal connection time."); 219 mWifiThreadRunner.post(() -> 220 mWifiDiagnostics.takeBugReport(bugTitle, bugDetail), 221 TAG + "#" + msg.what); 222 } 223 } 224 // Should reset last connection time after each connection regardless if bugreport 225 // is enabled or not. 226 mNetworkIdToLastStartConnectTimeMillisSinceBoot.remove(networkId); 227 break; 228 } 229 default: 230 return; 231 } 232 } 233 234 /** 235 * Refreshes recentAvailableNetworks with the latest available networks 236 * Adds new networks, removes old ones that have timed out. Should be called after Wifi 237 * framework decides what networks it is potentially connecting to. 238 * @param availableNetworks ScanDetail & Config list of potential connection 239 * candidates 240 */ updateAvailableNetworks( List<Pair<ScanDetail, WifiConfiguration>> availableNetworks)241 public void updateAvailableNetworks( 242 List<Pair<ScanDetail, WifiConfiguration>> availableNetworks) { 243 // Add new networks to mRecentAvailableNetworks 244 if (availableNetworks != null) { 245 if (mVerboseLoggingEnabled) { 246 Log.v(TAG, "updateAvailableNetworks: size = " + availableNetworks.size()); 247 } 248 for (Pair<ScanDetail, WifiConfiguration> pair : availableNetworks) { 249 final ScanDetail scanDetail = pair.first; 250 final WifiConfiguration config = pair.second; 251 ScanResult scanResult = scanDetail.getScanResult(); 252 if (scanResult == null) continue; 253 String bssid = scanResult.BSSID; 254 String ssid = "\"" + scanDetail.getSSID() + "\""; 255 if (mVerboseLoggingEnabled) { 256 Log.v(TAG, " " + bssid + ": " + scanDetail.getSSID()); 257 } 258 // Cache the scanResult & WifiConfig 259 AvailableNetworkFailureCount availableNetworkFailureCount = 260 mRecentAvailableNetworks.get(bssid); 261 if (availableNetworkFailureCount == null) { 262 // New network is available 263 availableNetworkFailureCount = new AvailableNetworkFailureCount(config); 264 availableNetworkFailureCount.ssid = ssid; 265 266 // Count AP for this SSID 267 Pair<AvailableNetworkFailureCount, Integer> ssidFailsAndApCount = 268 mSsidFailureCount.get(ssid); 269 if (ssidFailsAndApCount == null) { 270 // This is a new SSID, create new FailureCount for it and set AP count to 1 271 ssidFailsAndApCount = Pair.create(new AvailableNetworkFailureCount(config), 272 1); 273 // Do not re-enable Watchdog in LAST_TRIGGER_TIMEOUT_MILLIS 274 // after last time Watchdog be triggered 275 if (!mWatchdogAllowedToTrigger && (mTimeLastTrigger == 0 276 || (mClock.getElapsedSinceBootMillis() - mTimeLastTrigger) 277 >= LAST_TRIGGER_TIMEOUT_MILLIS)) { 278 localLog("updateAvailableNetworks: setWatchdogTriggerEnabled to true"); 279 setWatchdogTriggerEnabled(true); 280 } 281 } else { 282 final Integer numberOfAps = ssidFailsAndApCount.second; 283 // This is not a new SSID, increment the AP count for it 284 ssidFailsAndApCount = Pair.create(ssidFailsAndApCount.first, 285 numberOfAps + 1); 286 } 287 mSsidFailureCount.put(ssid, ssidFailsAndApCount); 288 } 289 // refresh config if it is not null 290 if (config != null) { 291 availableNetworkFailureCount.config = config; 292 } 293 // If we saw a network, set its Age to -1 here, aging iteration will set it to 0 294 availableNetworkFailureCount.age = -1; 295 mRecentAvailableNetworks.put(bssid, availableNetworkFailureCount); 296 } 297 } 298 299 // Iterate through available networks updating timeout counts & removing networks. 300 Iterator<Map.Entry<String, AvailableNetworkFailureCount>> it = 301 mRecentAvailableNetworks.entrySet().iterator(); 302 while (it.hasNext()) { 303 Map.Entry<String, AvailableNetworkFailureCount> entry = it.next(); 304 if (entry.getValue().age < MAX_BSSID_AGE - 1) { 305 entry.getValue().age++; 306 } else { 307 // Decrement this SSID : AP count 308 String ssid = entry.getValue().ssid; 309 Pair<AvailableNetworkFailureCount, Integer> ssidFails = 310 mSsidFailureCount.get(ssid); 311 if (ssidFails != null) { 312 Integer apCount = ssidFails.second - 1; 313 if (apCount > 0) { 314 ssidFails = Pair.create(ssidFails.first, apCount); 315 mSsidFailureCount.put(ssid, ssidFails); 316 } else { 317 mSsidFailureCount.remove(ssid); 318 } 319 } else { 320 Log.d(TAG, "updateAvailableNetworks: SSID to AP count mismatch for " + ssid); 321 } 322 it.remove(); 323 } 324 } 325 if (mVerboseLoggingEnabled) Log.v(TAG, toString()); 326 } 327 328 /** 329 * Increments the failure reason count for the given bssid. Performs a check to see if we have 330 * exceeded a failure threshold for all available networks, and executes the last resort restart 331 * @param bssid of the network that has failed connection, can be "any" 332 * @param reason Message id from ClientModeImpl for this failure 333 * @param isConnected whether the ClientModeImpl is currently connected 334 * @return true if watchdog triggers, returned for test visibility 335 */ noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason, boolean isConnected)336 public boolean noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason, 337 boolean isConnected) { 338 if (mVerboseLoggingEnabled) { 339 Log.v(TAG, "noteConnectionFailureAndTriggerIfNeeded: [" + ssid + ", " + bssid + ", " 340 + reason + "]"); 341 } 342 343 // Update failure count for the failing network 344 updateFailureCountForNetwork(ssid, bssid, reason); 345 346 // If watchdog is not allowed to trigger it means a wifi restart is already triggered 347 if (!mWatchdogAllowedToTrigger) { 348 mWifiMetrics.incrementWatchdogTotalConnectionFailureCountAfterTrigger(); 349 mWatchdogFixedWifi = false; 350 } 351 // Have we met conditions to trigger the Watchdog Wifi restart? 352 boolean isRestartNeeded = checkTriggerCondition(isConnected); 353 if (mVerboseLoggingEnabled) { 354 Log.v(TAG, "isRestartNeeded = " + isRestartNeeded); 355 } 356 if (isRestartNeeded) { 357 if (getWifiWatchdogFeature()) { 358 // Stop the watchdog from triggering until re-enabled 359 localLog("Trigger recovery: setWatchdogTriggerEnabled to false"); 360 setWatchdogTriggerEnabled(false); 361 mWatchdogFixedWifi = true; 362 loge("Watchdog triggering recovery"); 363 mSsidLastTrigger = ssid; 364 mTimeLastTrigger = mClock.getElapsedSinceBootMillis(); 365 localLog(toString()); 366 mWifiInjector.getSelfRecovery().trigger(SelfRecovery.REASON_LAST_RESORT_WATCHDOG); 367 incrementWifiMetricsTriggerCounts(); 368 } else { 369 // auto bugreport if issue happens 370 loge("bugreport notification"); 371 setWatchdogTriggerEnabled(false); 372 takeBugReportWithCurrentProbability("Wifi Watchdog bite"); 373 } 374 } 375 return isRestartNeeded; 376 } 377 378 /** 379 * Handles transitions entering and exiting ClientModeImpl ConnectedState 380 * Used to track wifistate, and perform watchdog count resetting 381 * @param isEntering true if called from ConnectedState.enter(), false for exit() 382 */ connectedStateTransition(boolean isEntering)383 public void connectedStateTransition(boolean isEntering) { 384 logv("connectedStateTransition: isEntering = " + isEntering); 385 386 if (!isEntering) { 387 return; 388 } 389 WifiInfo wifiInfo = getPrimaryWifiInfo(); 390 if (!mWatchdogAllowedToTrigger && mWatchdogFixedWifi 391 && getWifiWatchdogFeature() 392 && checkIfAtleastOneNetworkHasEverConnected() 393 && checkIfConnectedBackToSameSsid(wifiInfo) 394 && checkIfConnectedBssidHasEverFailed(wifiInfo)) { 395 takeBugReportWithCurrentProbability("Wifi fixed after restart"); 396 // WiFi has connected after a Watchdog trigger, without any new networks becoming 397 // available, log a Watchdog success in wifi metrics 398 mWifiMetrics.incrementNumLastResortWatchdogSuccesses(); 399 long durationMs = mClock.getElapsedSinceBootMillis() - mTimeLastTrigger; 400 mWifiMetrics.setWatchdogSuccessTimeDurationMs(durationMs); 401 } 402 // If the watchdog trigger was disabled (it triggered), connecting means we did 403 // something right, re-enable it so it can fire again. 404 localLog("connectedStateTransition: setWatchdogTriggerEnabled to true"); 405 setWatchdogTriggerEnabled(true); 406 } 407 408 /** 409 * Helper function to check if device connected to BSSID 410 * which is in BSSID failure list after watchdog trigger. 411 */ checkIfConnectedBssidHasEverFailed(@onNull WifiInfo wifiInfo)412 private boolean checkIfConnectedBssidHasEverFailed(@NonNull WifiInfo wifiInfo) { 413 return mBssidFailureList.contains(wifiInfo.getBSSID()); 414 } 415 416 /** 417 * Helper function to check if device connect back to same 418 * SSID after watchdog trigger 419 */ checkIfConnectedBackToSameSsid(@onNull WifiInfo wifiInfo)420 private boolean checkIfConnectedBackToSameSsid(@NonNull WifiInfo wifiInfo) { 421 if (TextUtils.equals(mSsidLastTrigger, wifiInfo.getSSID())) { 422 return true; 423 } 424 localLog("checkIfConnectedBackToSameSsid: different SSID be connected"); 425 return false; 426 } 427 428 /** 429 * Triggers a wifi specific bugreport with a based on the current trigger probability. 430 * @param bugDetail description of the bug 431 */ takeBugReportWithCurrentProbability(String bugDetail)432 private void takeBugReportWithCurrentProbability(String bugDetail) { 433 if (mBugReportProbability <= Math.random()) { 434 return; 435 } 436 mHandler.post(() -> mWifiDiagnostics.takeBugReport(BUGREPORT_TITLE, bugDetail)); 437 } 438 439 /** 440 * Increments the failure reason count for the given network, in 'mSsidFailureCount' 441 * Failures are counted per SSID, either; by using the ssid string when the bssid is "any" 442 * or by looking up the ssid attached to a specific bssid 443 * An unused set of counts is also kept which is bssid specific, in 'mRecentAvailableNetworks' 444 * @param ssid of the network that has failed connection 445 * @param bssid of the network that has failed connection, can be "any" 446 * @param reason Message id from ClientModeImpl for this failure 447 */ updateFailureCountForNetwork(String ssid, String bssid, int reason)448 private void updateFailureCountForNetwork(String ssid, String bssid, int reason) { 449 logv("updateFailureCountForNetwork: [" + ssid + ", " + bssid + ", " 450 + reason + "]"); 451 if (BSSID_ANY.equals(bssid)) { 452 incrementSsidFailureCount(ssid, reason); 453 } else { 454 // Bssid count is actually unused except for logging purposes 455 // SSID count is incremented within the BSSID counting method 456 incrementBssidFailureCount(ssid, bssid, reason); 457 mBssidFailureList.add(bssid); 458 } 459 } 460 461 /** 462 * Update the per-SSID failure count 463 * @param ssid the ssid to increment failure count for 464 * @param reason the failure type to increment count for 465 */ incrementSsidFailureCount(String ssid, int reason)466 private void incrementSsidFailureCount(String ssid, int reason) { 467 Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid); 468 if (ssidFails == null) { 469 Log.d(TAG, "updateFailureCountForNetwork: No networks for ssid = " + ssid); 470 return; 471 } 472 AvailableNetworkFailureCount failureCount = ssidFails.first; 473 failureCount.incrementFailureCount(reason); 474 } 475 476 /** 477 * Update the per-BSSID failure count 478 * @param bssid the bssid to increment failure count for 479 * @param reason the failure type to increment count for 480 */ incrementBssidFailureCount(String ssid, String bssid, int reason)481 private void incrementBssidFailureCount(String ssid, String bssid, int reason) { 482 AvailableNetworkFailureCount availableNetworkFailureCount = 483 mRecentAvailableNetworks.get(bssid); 484 if (availableNetworkFailureCount == null) { 485 Log.d(TAG, "updateFailureCountForNetwork: Unable to find Network [" + ssid 486 + ", " + bssid + "]"); 487 return; 488 } 489 if (!availableNetworkFailureCount.ssid.equals(ssid)) { 490 Log.d(TAG, "updateFailureCountForNetwork: Failed connection attempt has" 491 + " wrong ssid. Failed [" + ssid + ", " + bssid + "], buffered [" 492 + availableNetworkFailureCount.ssid + ", " + bssid + "]"); 493 return; 494 } 495 if (availableNetworkFailureCount.config == null) { 496 if (mVerboseLoggingEnabled) { 497 Log.v(TAG, "updateFailureCountForNetwork: network has no config [" 498 + ssid + ", " + bssid + "]"); 499 } 500 } 501 availableNetworkFailureCount.incrementFailureCount(reason); 502 incrementSsidFailureCount(ssid, reason); 503 } 504 505 /** 506 * Helper function to check if we should ignore BSSID update. 507 * @param bssid BSSID of the access point 508 * @return true if we should ignore BSSID update 509 */ shouldIgnoreBssidUpdate(String bssid)510 public boolean shouldIgnoreBssidUpdate(String bssid) { 511 return mWatchdogAllowedToTrigger 512 && isBssidOnlyApOfSsid(bssid) 513 && isSingleSsidRecorded() 514 && checkIfAtleastOneNetworkHasEverConnected(); 515 } 516 517 /** 518 * Helper function to check if we should ignore SSID update. 519 * @return true if should ignore SSID update 520 */ shouldIgnoreSsidUpdate()521 public boolean shouldIgnoreSsidUpdate() { 522 return mWatchdogAllowedToTrigger 523 && isSingleSsidRecorded() 524 && checkIfAtleastOneNetworkHasEverConnected(); 525 } 526 527 /** 528 * Check the specified BSSID is the only BSSID for its corresponding SSID. 529 * @param bssid BSSID of the access point 530 * @return true if only BSSID for its corresponding SSID be observed 531 */ isBssidOnlyApOfSsid(String bssid)532 public boolean isBssidOnlyApOfSsid(String bssid) { 533 AvailableNetworkFailureCount availableNetworkFailureCount = 534 mRecentAvailableNetworks.get(bssid); 535 if (availableNetworkFailureCount == null) { 536 return false; 537 } 538 String ssid = availableNetworkFailureCount.ssid; 539 Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid); 540 if (ssidFails == null) { 541 Log.d(TAG, "isOnlyBssidAvailable: Could not find SSID count for " + ssid); 542 return false; 543 } 544 if (ssidFails.second != 1) { 545 return false; 546 } 547 return true; 548 } 549 550 /** 551 * Check there is only single SSID be observed. 552 * @return true if only single SSID be observed. 553 */ isSingleSsidRecorded()554 private boolean isSingleSsidRecorded() { 555 return (mSsidFailureCount.size() == 1); 556 } 557 558 /** 559 * Check trigger condition: For all available networks, have we met a failure threshold for each 560 * of them, and have previously connected to at-least one of the available networks 561 * @return is the trigger condition true 562 */ checkTriggerCondition(boolean isConnected)563 private boolean checkTriggerCondition(boolean isConnected) { 564 if (mVerboseLoggingEnabled) Log.v(TAG, "checkTriggerCondition."); 565 // Don't check Watchdog trigger if wifi is in a connected state 566 // (This should not occur, but we want to protect against any race conditions) 567 if (isConnected) return false; 568 // Don't check Watchdog trigger if trigger is not enabled 569 if (!mWatchdogAllowedToTrigger) return false; 570 571 for (Map.Entry<String, AvailableNetworkFailureCount> entry 572 : mRecentAvailableNetworks.entrySet()) { 573 if (!isOverFailureThreshold(entry.getKey())) { 574 // This available network is not over failure threshold, meaning we still have a 575 // network to try connecting to 576 return false; 577 } 578 } 579 // We have met the failure count for every available network. 580 // Trigger restart if there exists at-least one network that we have previously connected. 581 boolean atleastOneNetworkHasEverConnected = checkIfAtleastOneNetworkHasEverConnected(); 582 logv("checkTriggerCondition: return = " + atleastOneNetworkHasEverConnected); 583 return checkIfAtleastOneNetworkHasEverConnected(); 584 } 585 checkIfAtleastOneNetworkHasEverConnected()586 private boolean checkIfAtleastOneNetworkHasEverConnected() { 587 for (Map.Entry<String, AvailableNetworkFailureCount> entry 588 : mRecentAvailableNetworks.entrySet()) { 589 if (entry.getValue().config != null 590 && entry.getValue().config.getNetworkSelectionStatus().hasEverConnected()) { 591 return true; 592 } 593 } 594 return false; 595 } 596 597 /** 598 * Update WifiMetrics with various Watchdog stats (trigger counts, failed network counts) 599 */ incrementWifiMetricsTriggerCounts()600 private void incrementWifiMetricsTriggerCounts() { 601 if (mVerboseLoggingEnabled) Log.v(TAG, "incrementWifiMetricsTriggerCounts."); 602 mWifiMetrics.incrementNumLastResortWatchdogTriggers(); 603 mWifiMetrics.addCountToNumLastResortWatchdogAvailableNetworksTotal( 604 mSsidFailureCount.size()); 605 // Number of networks over each failure type threshold, present at trigger time 606 int badAuth = 0; 607 int badAssoc = 0; 608 int badDhcp = 0; 609 int badSum = 0; 610 for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry 611 : mSsidFailureCount.entrySet()) { 612 badSum = entry.getValue().first.associationRejection 613 + entry.getValue().first.authenticationFailure 614 + entry.getValue().first.dhcpFailure; 615 // count as contributor if over half of badSum. 616 if (badSum >= FAILURE_THRESHOLD) { 617 badAssoc += (entry.getValue().first.associationRejection >= badSum / 2) ? 1 : 0; 618 badAuth += (entry.getValue().first.authenticationFailure >= badSum / 2) ? 1 : 0; 619 badDhcp += (entry.getValue().first.dhcpFailure >= badSum / 2) ? 1 : 0; 620 } 621 } 622 if (badAuth > 0) { 623 mWifiMetrics.addCountToNumLastResortWatchdogBadAuthenticationNetworksTotal(badAuth); 624 mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAuthentication(); 625 } 626 if (badAssoc > 0) { 627 mWifiMetrics.addCountToNumLastResortWatchdogBadAssociationNetworksTotal(badAssoc); 628 mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAssociation(); 629 } 630 if (badDhcp > 0) { 631 mWifiMetrics.addCountToNumLastResortWatchdogBadDhcpNetworksTotal(badDhcp); 632 mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadDhcp(); 633 } 634 } 635 636 /** 637 * Clear all failure counts 638 */ clearAllFailureCounts()639 public void clearAllFailureCounts() { 640 if (mVerboseLoggingEnabled) Log.v(TAG, "clearAllFailureCounts."); 641 for (Map.Entry<String, AvailableNetworkFailureCount> entry 642 : mRecentAvailableNetworks.entrySet()) { 643 final AvailableNetworkFailureCount failureCount = entry.getValue(); 644 failureCount.resetCounts(); 645 } 646 for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry 647 : mSsidFailureCount.entrySet()) { 648 final AvailableNetworkFailureCount failureCount = entry.getValue().first; 649 failureCount.resetCounts(); 650 } 651 mBssidFailureList.clear(); 652 } 653 /** 654 * Gets the buffer of recently available networks 655 */ 656 @VisibleForTesting getRecentAvailableNetworks()657 Map<String, AvailableNetworkFailureCount> getRecentAvailableNetworks() { 658 return mRecentAvailableNetworks; 659 } 660 661 /** 662 * Activates or deactivates the Watchdog trigger. Counting and network buffering still occurs 663 * @param enable true to enable the Watchdog trigger, false to disable it 664 */ setWatchdogTriggerEnabled(boolean enable)665 private void setWatchdogTriggerEnabled(boolean enable) { 666 if (mVerboseLoggingEnabled) Log.v(TAG, "setWatchdogTriggerEnabled: enable = " + enable); 667 // Reset failure counts before actives watchdog 668 if (enable) { 669 clearAllFailureCounts(); 670 } 671 mWatchdogAllowedToTrigger = enable; 672 } 673 674 /** 675 * Prints all networks & counts within mRecentAvailableNetworks to string 676 */ 677 @Override toString()678 public String toString() { 679 StringBuilder sb = new StringBuilder(); 680 sb.append("mWatchdogFeatureEnabled: ").append(getWifiWatchdogFeature()); 681 sb.append("\nmWatchdogAllowedToTrigger: ").append(mWatchdogAllowedToTrigger); 682 sb.append("\nmRecentAvailableNetworks: ").append(mRecentAvailableNetworks.size()); 683 for (Map.Entry<String, AvailableNetworkFailureCount> entry 684 : mRecentAvailableNetworks.entrySet()) { 685 sb.append("\n ").append(entry.getKey()).append(": ").append(entry.getValue()) 686 .append(", Age: ").append(entry.getValue().age); 687 } 688 sb.append("\nmSsidFailureCount:"); 689 for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry : 690 mSsidFailureCount.entrySet()) { 691 final AvailableNetworkFailureCount failureCount = entry.getValue().first; 692 final Integer apCount = entry.getValue().second; 693 sb.append("\n").append(entry.getKey()).append(": ").append(apCount).append(",") 694 .append(failureCount.toString()); 695 } 696 return sb.toString(); 697 } 698 699 /** 700 * @param bssid bssid to check the failures for 701 * @return true if sum of failure count is over FAILURE_THRESHOLD 702 */ 703 @VisibleForTesting isOverFailureThreshold(String bssid)704 boolean isOverFailureThreshold(String bssid) { 705 return (getFailureCount(bssid, FAILURE_CODE_ASSOCIATION) 706 + getFailureCount(bssid, FAILURE_CODE_AUTHENTICATION) 707 + getFailureCount(bssid, FAILURE_CODE_DHCP)) >= FAILURE_THRESHOLD; 708 } 709 710 /** 711 * Get the failure count for a specific bssid. This actually checks the ssid attached to the 712 * BSSID and returns the SSID count 713 * @param reason failure reason to get count for 714 */ 715 @VisibleForTesting getFailureCount(String bssid, int reason)716 int getFailureCount(String bssid, int reason) { 717 AvailableNetworkFailureCount availableNetworkFailureCount = 718 mRecentAvailableNetworks.get(bssid); 719 if (availableNetworkFailureCount == null) { 720 return 0; 721 } 722 String ssid = availableNetworkFailureCount.ssid; 723 Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid); 724 if (ssidFails == null) { 725 Log.d(TAG, "getFailureCount: Could not find SSID count for " + ssid); 726 return 0; 727 } 728 final AvailableNetworkFailureCount failCount = ssidFails.first; 729 switch (reason) { 730 case FAILURE_CODE_ASSOCIATION: 731 return failCount.associationRejection; 732 case FAILURE_CODE_AUTHENTICATION: 733 return failCount.authenticationFailure; 734 case FAILURE_CODE_DHCP: 735 return failCount.dhcpFailure; 736 default: 737 return 0; 738 } 739 } 740 741 /** 742 * Sets whether wifi watchdog should trigger recovery 743 */ setWifiWatchdogFeature(boolean enable)744 public void setWifiWatchdogFeature(boolean enable) { 745 logv("setWifiWatchdogFeature: " + enable); 746 mWatchdogFeatureEnabled = enable; 747 // for debugging purpose, reset mWatchdogAllowedToTrigger as well 748 setWatchdogTriggerEnabled(true); 749 } 750 751 /** 752 * Returns whether wifi watchdog should trigger recovery. 753 */ getWifiWatchdogFeature()754 public boolean getWifiWatchdogFeature() { 755 if (mWatchdogFeatureEnabled == null) { 756 mWatchdogFeatureEnabled = mContext.getResources().getBoolean( 757 R.bool.config_wifi_watchdog_enabled); 758 } 759 return mWatchdogFeatureEnabled; 760 } 761 762 /** Enable/disable verbose logging. */ enableVerboseLogging(boolean verboseEnabled)763 public void enableVerboseLogging(boolean verboseEnabled) { 764 mVerboseLoggingEnabled = verboseEnabled; 765 } 766 767 @VisibleForTesting setBugReportProbability(double newProbability)768 void setBugReportProbability(double newProbability) { 769 mBugReportProbability = newProbability; 770 } 771 772 /** 773 * This class holds the failure counts for an 'available network' (one of the potential 774 * candidates for connection, as determined by framework). 775 */ 776 public static class AvailableNetworkFailureCount { 777 /** 778 * WifiConfiguration associated with this network. Can be null for Ephemeral networks 779 */ 780 public WifiConfiguration config; 781 /** 782 * SSID of the network (from ScanDetail) 783 */ 784 public String ssid = ""; 785 /** 786 * Number of times network has failed due to Association Rejection 787 */ 788 public int associationRejection = 0; 789 /** 790 * Number of times network has failed due to Authentication Failure or SSID_TEMP_DISABLED 791 */ 792 public int authenticationFailure = 0; 793 /** 794 * Number of times network has failed due to DHCP failure 795 */ 796 public int dhcpFailure = 0; 797 /** 798 * Number of scanResults since this network was last seen 799 */ 800 public int age = 0; 801 AvailableNetworkFailureCount(WifiConfiguration configParam)802 AvailableNetworkFailureCount(WifiConfiguration configParam) { 803 this.config = configParam; 804 } 805 806 /** 807 * @param reason failure reason to increment count for 808 */ incrementFailureCount(int reason)809 public void incrementFailureCount(int reason) { 810 switch (reason) { 811 case FAILURE_CODE_ASSOCIATION: 812 associationRejection++; 813 break; 814 case FAILURE_CODE_AUTHENTICATION: 815 authenticationFailure++; 816 break; 817 case FAILURE_CODE_DHCP: 818 dhcpFailure++; 819 break; 820 default: //do nothing 821 } 822 } 823 824 /** 825 * Set all failure counts for this network to 0 826 */ resetCounts()827 void resetCounts() { 828 associationRejection = 0; 829 authenticationFailure = 0; 830 dhcpFailure = 0; 831 } 832 toString()833 public String toString() { 834 return ssid + " HasEverConnected: " + ((config != null) 835 ? config.getNetworkSelectionStatus().hasEverConnected() : "null_config") 836 + ", Failures: {" 837 + "Assoc: " + associationRejection 838 + ", Auth: " + authenticationFailure 839 + ", Dhcp: " + dhcpFailure 840 + "}"; 841 } 842 } 843 844 /** 845 * Helper function for logging into local log buffer. 846 */ localLog(String s)847 private void localLog(String s) { 848 mLocalLog.log(s); 849 } 850 logv(String s)851 private void logv(String s) { 852 mLocalLog.log(s); 853 if (mVerboseLoggingEnabled) { 854 Log.v(TAG, s, null); 855 } 856 } 857 loge(String s)858 private void loge(String s) { 859 mLocalLog.log(s); 860 Log.e(TAG, s, null); 861 } 862 863 /** 864 * Dump the local log buffer and other internal state of WifiLastResortWatchdog. 865 */ dump(FileDescriptor fd, PrintWriter pw, String[] args)866 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 867 pw.println("Dump of WifiLastResortWatchdog"); 868 pw.println("WifiLastResortWatchdog - Log Begin ----"); 869 mLocalLog.dump(fd, pw, args); 870 pw.println("WifiLastResortWatchdog - Log End ----"); 871 } 872 } 873