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.syncRequestConnectionInfo(); 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 } 222 } 223 // Should reset last connection time after each connection regardless if bugreport 224 // is enabled or not. 225 mNetworkIdToLastStartConnectTimeMillisSinceBoot.remove(networkId); 226 break; 227 } 228 default: 229 return; 230 } 231 } 232 233 /** 234 * Refreshes recentAvailableNetworks with the latest available networks 235 * Adds new networks, removes old ones that have timed out. Should be called after Wifi 236 * framework decides what networks it is potentially connecting to. 237 * @param availableNetworks ScanDetail & Config list of potential connection 238 * candidates 239 */ updateAvailableNetworks( List<Pair<ScanDetail, WifiConfiguration>> availableNetworks)240 public void updateAvailableNetworks( 241 List<Pair<ScanDetail, WifiConfiguration>> availableNetworks) { 242 // Add new networks to mRecentAvailableNetworks 243 if (availableNetworks != null) { 244 if (mVerboseLoggingEnabled) { 245 Log.v(TAG, "updateAvailableNetworks: size = " + availableNetworks.size()); 246 } 247 for (Pair<ScanDetail, WifiConfiguration> pair : availableNetworks) { 248 final ScanDetail scanDetail = pair.first; 249 final WifiConfiguration config = pair.second; 250 ScanResult scanResult = scanDetail.getScanResult(); 251 if (scanResult == null) continue; 252 String bssid = scanResult.BSSID; 253 String ssid = "\"" + scanDetail.getSSID() + "\""; 254 if (mVerboseLoggingEnabled) { 255 Log.v(TAG, " " + bssid + ": " + scanDetail.getSSID()); 256 } 257 // Cache the scanResult & WifiConfig 258 AvailableNetworkFailureCount availableNetworkFailureCount = 259 mRecentAvailableNetworks.get(bssid); 260 if (availableNetworkFailureCount == null) { 261 // New network is available 262 availableNetworkFailureCount = new AvailableNetworkFailureCount(config); 263 availableNetworkFailureCount.ssid = ssid; 264 265 // Count AP for this SSID 266 Pair<AvailableNetworkFailureCount, Integer> ssidFailsAndApCount = 267 mSsidFailureCount.get(ssid); 268 if (ssidFailsAndApCount == null) { 269 // This is a new SSID, create new FailureCount for it and set AP count to 1 270 ssidFailsAndApCount = Pair.create(new AvailableNetworkFailureCount(config), 271 1); 272 // Do not re-enable Watchdog in LAST_TRIGGER_TIMEOUT_MILLIS 273 // after last time Watchdog be triggered 274 if (!mWatchdogAllowedToTrigger && (mTimeLastTrigger == 0 275 || (mClock.getElapsedSinceBootMillis() - mTimeLastTrigger) 276 >= LAST_TRIGGER_TIMEOUT_MILLIS)) { 277 localLog("updateAvailableNetworks: setWatchdogTriggerEnabled to true"); 278 setWatchdogTriggerEnabled(true); 279 } 280 } else { 281 final Integer numberOfAps = ssidFailsAndApCount.second; 282 // This is not a new SSID, increment the AP count for it 283 ssidFailsAndApCount = Pair.create(ssidFailsAndApCount.first, 284 numberOfAps + 1); 285 } 286 mSsidFailureCount.put(ssid, ssidFailsAndApCount); 287 } 288 // refresh config if it is not null 289 if (config != null) { 290 availableNetworkFailureCount.config = config; 291 } 292 // If we saw a network, set its Age to -1 here, aging iteration will set it to 0 293 availableNetworkFailureCount.age = -1; 294 mRecentAvailableNetworks.put(bssid, availableNetworkFailureCount); 295 } 296 } 297 298 // Iterate through available networks updating timeout counts & removing networks. 299 Iterator<Map.Entry<String, AvailableNetworkFailureCount>> it = 300 mRecentAvailableNetworks.entrySet().iterator(); 301 while (it.hasNext()) { 302 Map.Entry<String, AvailableNetworkFailureCount> entry = it.next(); 303 if (entry.getValue().age < MAX_BSSID_AGE - 1) { 304 entry.getValue().age++; 305 } else { 306 // Decrement this SSID : AP count 307 String ssid = entry.getValue().ssid; 308 Pair<AvailableNetworkFailureCount, Integer> ssidFails = 309 mSsidFailureCount.get(ssid); 310 if (ssidFails != null) { 311 Integer apCount = ssidFails.second - 1; 312 if (apCount > 0) { 313 ssidFails = Pair.create(ssidFails.first, apCount); 314 mSsidFailureCount.put(ssid, ssidFails); 315 } else { 316 mSsidFailureCount.remove(ssid); 317 } 318 } else { 319 Log.d(TAG, "updateAvailableNetworks: SSID to AP count mismatch for " + ssid); 320 } 321 it.remove(); 322 } 323 } 324 if (mVerboseLoggingEnabled) Log.v(TAG, toString()); 325 } 326 327 /** 328 * Increments the failure reason count for the given bssid. Performs a check to see if we have 329 * exceeded a failure threshold for all available networks, and executes the last resort restart 330 * @param bssid of the network that has failed connection, can be "any" 331 * @param reason Message id from ClientModeImpl for this failure 332 * @param isConnected whether the ClientModeImpl is currently connected 333 * @return true if watchdog triggers, returned for test visibility 334 */ noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason, boolean isConnected)335 public boolean noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason, 336 boolean isConnected) { 337 if (mVerboseLoggingEnabled) { 338 Log.v(TAG, "noteConnectionFailureAndTriggerIfNeeded: [" + ssid + ", " + bssid + ", " 339 + reason + "]"); 340 } 341 342 // Update failure count for the failing network 343 updateFailureCountForNetwork(ssid, bssid, reason); 344 345 // If watchdog is not allowed to trigger it means a wifi restart is already triggered 346 if (!mWatchdogAllowedToTrigger) { 347 mWifiMetrics.incrementWatchdogTotalConnectionFailureCountAfterTrigger(); 348 mWatchdogFixedWifi = false; 349 } 350 // Have we met conditions to trigger the Watchdog Wifi restart? 351 boolean isRestartNeeded = checkTriggerCondition(isConnected); 352 if (mVerboseLoggingEnabled) { 353 Log.v(TAG, "isRestartNeeded = " + isRestartNeeded); 354 } 355 if (isRestartNeeded) { 356 if (getWifiWatchdogFeature()) { 357 // Stop the watchdog from triggering until re-enabled 358 localLog("Trigger recovery: setWatchdogTriggerEnabled to false"); 359 setWatchdogTriggerEnabled(false); 360 mWatchdogFixedWifi = true; 361 loge("Watchdog triggering recovery"); 362 mSsidLastTrigger = ssid; 363 mTimeLastTrigger = mClock.getElapsedSinceBootMillis(); 364 localLog(toString()); 365 mWifiInjector.getSelfRecovery().trigger(SelfRecovery.REASON_LAST_RESORT_WATCHDOG); 366 incrementWifiMetricsTriggerCounts(); 367 } else { 368 // auto bugreport if issue happens 369 loge("bugreport notification"); 370 setWatchdogTriggerEnabled(false); 371 takeBugReportWithCurrentProbability("Wifi Watchdog bite"); 372 } 373 } 374 return isRestartNeeded; 375 } 376 377 /** 378 * Handles transitions entering and exiting ClientModeImpl ConnectedState 379 * Used to track wifistate, and perform watchdog count resetting 380 * @param isEntering true if called from ConnectedState.enter(), false for exit() 381 */ connectedStateTransition(boolean isEntering)382 public void connectedStateTransition(boolean isEntering) { 383 logv("connectedStateTransition: isEntering = " + isEntering); 384 385 if (!isEntering) { 386 return; 387 } 388 WifiInfo wifiInfo = getPrimaryWifiInfo(); 389 if (!mWatchdogAllowedToTrigger && mWatchdogFixedWifi 390 && getWifiWatchdogFeature() 391 && checkIfAtleastOneNetworkHasEverConnected() 392 && checkIfConnectedBackToSameSsid(wifiInfo) 393 && checkIfConnectedBssidHasEverFailed(wifiInfo)) { 394 takeBugReportWithCurrentProbability("Wifi fixed after restart"); 395 // WiFi has connected after a Watchdog trigger, without any new networks becoming 396 // available, log a Watchdog success in wifi metrics 397 mWifiMetrics.incrementNumLastResortWatchdogSuccesses(); 398 long durationMs = mClock.getElapsedSinceBootMillis() - mTimeLastTrigger; 399 mWifiMetrics.setWatchdogSuccessTimeDurationMs(durationMs); 400 } 401 // If the watchdog trigger was disabled (it triggered), connecting means we did 402 // something right, re-enable it so it can fire again. 403 localLog("connectedStateTransition: setWatchdogTriggerEnabled to true"); 404 setWatchdogTriggerEnabled(true); 405 } 406 407 /** 408 * Helper function to check if device connected to BSSID 409 * which is in BSSID failure list after watchdog trigger. 410 */ checkIfConnectedBssidHasEverFailed(@onNull WifiInfo wifiInfo)411 private boolean checkIfConnectedBssidHasEverFailed(@NonNull WifiInfo wifiInfo) { 412 return mBssidFailureList.contains(wifiInfo.getBSSID()); 413 } 414 415 /** 416 * Helper function to check if device connect back to same 417 * SSID after watchdog trigger 418 */ checkIfConnectedBackToSameSsid(@onNull WifiInfo wifiInfo)419 private boolean checkIfConnectedBackToSameSsid(@NonNull WifiInfo wifiInfo) { 420 if (TextUtils.equals(mSsidLastTrigger, wifiInfo.getSSID())) { 421 return true; 422 } 423 localLog("checkIfConnectedBackToSameSsid: different SSID be connected"); 424 return false; 425 } 426 427 /** 428 * Triggers a wifi specific bugreport with a based on the current trigger probability. 429 * @param bugDetail description of the bug 430 */ takeBugReportWithCurrentProbability(String bugDetail)431 private void takeBugReportWithCurrentProbability(String bugDetail) { 432 if (mBugReportProbability <= Math.random()) { 433 return; 434 } 435 mHandler.post(() -> mWifiDiagnostics.takeBugReport(BUGREPORT_TITLE, bugDetail)); 436 } 437 438 /** 439 * Increments the failure reason count for the given network, in 'mSsidFailureCount' 440 * Failures are counted per SSID, either; by using the ssid string when the bssid is "any" 441 * or by looking up the ssid attached to a specific bssid 442 * An unused set of counts is also kept which is bssid specific, in 'mRecentAvailableNetworks' 443 * @param ssid of the network that has failed connection 444 * @param bssid of the network that has failed connection, can be "any" 445 * @param reason Message id from ClientModeImpl for this failure 446 */ updateFailureCountForNetwork(String ssid, String bssid, int reason)447 private void updateFailureCountForNetwork(String ssid, String bssid, int reason) { 448 logv("updateFailureCountForNetwork: [" + ssid + ", " + bssid + ", " 449 + reason + "]"); 450 if (BSSID_ANY.equals(bssid)) { 451 incrementSsidFailureCount(ssid, reason); 452 } else { 453 // Bssid count is actually unused except for logging purposes 454 // SSID count is incremented within the BSSID counting method 455 incrementBssidFailureCount(ssid, bssid, reason); 456 mBssidFailureList.add(bssid); 457 } 458 } 459 460 /** 461 * Update the per-SSID failure count 462 * @param ssid the ssid to increment failure count for 463 * @param reason the failure type to increment count for 464 */ incrementSsidFailureCount(String ssid, int reason)465 private void incrementSsidFailureCount(String ssid, int reason) { 466 Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid); 467 if (ssidFails == null) { 468 Log.d(TAG, "updateFailureCountForNetwork: No networks for ssid = " + ssid); 469 return; 470 } 471 AvailableNetworkFailureCount failureCount = ssidFails.first; 472 failureCount.incrementFailureCount(reason); 473 } 474 475 /** 476 * Update the per-BSSID failure count 477 * @param bssid the bssid to increment failure count for 478 * @param reason the failure type to increment count for 479 */ incrementBssidFailureCount(String ssid, String bssid, int reason)480 private void incrementBssidFailureCount(String ssid, String bssid, int reason) { 481 AvailableNetworkFailureCount availableNetworkFailureCount = 482 mRecentAvailableNetworks.get(bssid); 483 if (availableNetworkFailureCount == null) { 484 Log.d(TAG, "updateFailureCountForNetwork: Unable to find Network [" + ssid 485 + ", " + bssid + "]"); 486 return; 487 } 488 if (!availableNetworkFailureCount.ssid.equals(ssid)) { 489 Log.d(TAG, "updateFailureCountForNetwork: Failed connection attempt has" 490 + " wrong ssid. Failed [" + ssid + ", " + bssid + "], buffered [" 491 + availableNetworkFailureCount.ssid + ", " + bssid + "]"); 492 return; 493 } 494 if (availableNetworkFailureCount.config == null) { 495 if (mVerboseLoggingEnabled) { 496 Log.v(TAG, "updateFailureCountForNetwork: network has no config [" 497 + ssid + ", " + bssid + "]"); 498 } 499 } 500 availableNetworkFailureCount.incrementFailureCount(reason); 501 incrementSsidFailureCount(ssid, reason); 502 } 503 504 /** 505 * Helper function to check if we should ignore BSSID update. 506 * @param bssid BSSID of the access point 507 * @return true if we should ignore BSSID update 508 */ shouldIgnoreBssidUpdate(String bssid)509 public boolean shouldIgnoreBssidUpdate(String bssid) { 510 return mWatchdogAllowedToTrigger 511 && isBssidOnlyApOfSsid(bssid) 512 && isSingleSsidRecorded() 513 && checkIfAtleastOneNetworkHasEverConnected(); 514 } 515 516 /** 517 * Helper function to check if we should ignore SSID update. 518 * @return true if should ignore SSID update 519 */ shouldIgnoreSsidUpdate()520 public boolean shouldIgnoreSsidUpdate() { 521 return mWatchdogAllowedToTrigger 522 && isSingleSsidRecorded() 523 && checkIfAtleastOneNetworkHasEverConnected(); 524 } 525 526 /** 527 * Check the specified BSSID is the only BSSID for its corresponding SSID. 528 * @param bssid BSSID of the access point 529 * @return true if only BSSID for its corresponding SSID be observed 530 */ isBssidOnlyApOfSsid(String bssid)531 public boolean isBssidOnlyApOfSsid(String bssid) { 532 AvailableNetworkFailureCount availableNetworkFailureCount = 533 mRecentAvailableNetworks.get(bssid); 534 if (availableNetworkFailureCount == null) { 535 return false; 536 } 537 String ssid = availableNetworkFailureCount.ssid; 538 Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid); 539 if (ssidFails == null) { 540 Log.d(TAG, "isOnlyBssidAvailable: Could not find SSID count for " + ssid); 541 return false; 542 } 543 if (ssidFails.second != 1) { 544 return false; 545 } 546 return true; 547 } 548 549 /** 550 * Check there is only single SSID be observed. 551 * @return true if only single SSID be observed. 552 */ isSingleSsidRecorded()553 private boolean isSingleSsidRecorded() { 554 return (mSsidFailureCount.size() == 1); 555 } 556 557 /** 558 * Check trigger condition: For all available networks, have we met a failure threshold for each 559 * of them, and have previously connected to at-least one of the available networks 560 * @return is the trigger condition true 561 */ checkTriggerCondition(boolean isConnected)562 private boolean checkTriggerCondition(boolean isConnected) { 563 if (mVerboseLoggingEnabled) Log.v(TAG, "checkTriggerCondition."); 564 // Don't check Watchdog trigger if wifi is in a connected state 565 // (This should not occur, but we want to protect against any race conditions) 566 if (isConnected) return false; 567 // Don't check Watchdog trigger if trigger is not enabled 568 if (!mWatchdogAllowedToTrigger) return false; 569 570 for (Map.Entry<String, AvailableNetworkFailureCount> entry 571 : mRecentAvailableNetworks.entrySet()) { 572 if (!isOverFailureThreshold(entry.getKey())) { 573 // This available network is not over failure threshold, meaning we still have a 574 // network to try connecting to 575 return false; 576 } 577 } 578 // We have met the failure count for every available network. 579 // Trigger restart if there exists at-least one network that we have previously connected. 580 boolean atleastOneNetworkHasEverConnected = checkIfAtleastOneNetworkHasEverConnected(); 581 logv("checkTriggerCondition: return = " + atleastOneNetworkHasEverConnected); 582 return checkIfAtleastOneNetworkHasEverConnected(); 583 } 584 checkIfAtleastOneNetworkHasEverConnected()585 private boolean checkIfAtleastOneNetworkHasEverConnected() { 586 for (Map.Entry<String, AvailableNetworkFailureCount> entry 587 : mRecentAvailableNetworks.entrySet()) { 588 if (entry.getValue().config != null 589 && entry.getValue().config.getNetworkSelectionStatus().hasEverConnected()) { 590 return true; 591 } 592 } 593 return false; 594 } 595 596 /** 597 * Update WifiMetrics with various Watchdog stats (trigger counts, failed network counts) 598 */ incrementWifiMetricsTriggerCounts()599 private void incrementWifiMetricsTriggerCounts() { 600 if (mVerboseLoggingEnabled) Log.v(TAG, "incrementWifiMetricsTriggerCounts."); 601 mWifiMetrics.incrementNumLastResortWatchdogTriggers(); 602 mWifiMetrics.addCountToNumLastResortWatchdogAvailableNetworksTotal( 603 mSsidFailureCount.size()); 604 // Number of networks over each failure type threshold, present at trigger time 605 int badAuth = 0; 606 int badAssoc = 0; 607 int badDhcp = 0; 608 int badSum = 0; 609 for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry 610 : mSsidFailureCount.entrySet()) { 611 badSum = entry.getValue().first.associationRejection 612 + entry.getValue().first.authenticationFailure 613 + entry.getValue().first.dhcpFailure; 614 // count as contributor if over half of badSum. 615 if (badSum >= FAILURE_THRESHOLD) { 616 badAssoc += (entry.getValue().first.associationRejection >= badSum / 2) ? 1 : 0; 617 badAuth += (entry.getValue().first.authenticationFailure >= badSum / 2) ? 1 : 0; 618 badDhcp += (entry.getValue().first.dhcpFailure >= badSum / 2) ? 1 : 0; 619 } 620 } 621 if (badAuth > 0) { 622 mWifiMetrics.addCountToNumLastResortWatchdogBadAuthenticationNetworksTotal(badAuth); 623 mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAuthentication(); 624 } 625 if (badAssoc > 0) { 626 mWifiMetrics.addCountToNumLastResortWatchdogBadAssociationNetworksTotal(badAssoc); 627 mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAssociation(); 628 } 629 if (badDhcp > 0) { 630 mWifiMetrics.addCountToNumLastResortWatchdogBadDhcpNetworksTotal(badDhcp); 631 mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadDhcp(); 632 } 633 } 634 635 /** 636 * Clear all failure counts 637 */ clearAllFailureCounts()638 public void clearAllFailureCounts() { 639 if (mVerboseLoggingEnabled) Log.v(TAG, "clearAllFailureCounts."); 640 for (Map.Entry<String, AvailableNetworkFailureCount> entry 641 : mRecentAvailableNetworks.entrySet()) { 642 final AvailableNetworkFailureCount failureCount = entry.getValue(); 643 failureCount.resetCounts(); 644 } 645 for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry 646 : mSsidFailureCount.entrySet()) { 647 final AvailableNetworkFailureCount failureCount = entry.getValue().first; 648 failureCount.resetCounts(); 649 } 650 mBssidFailureList.clear(); 651 } 652 /** 653 * Gets the buffer of recently available networks 654 */ 655 @VisibleForTesting getRecentAvailableNetworks()656 Map<String, AvailableNetworkFailureCount> getRecentAvailableNetworks() { 657 return mRecentAvailableNetworks; 658 } 659 660 /** 661 * Activates or deactivates the Watchdog trigger. Counting and network buffering still occurs 662 * @param enable true to enable the Watchdog trigger, false to disable it 663 */ setWatchdogTriggerEnabled(boolean enable)664 private void setWatchdogTriggerEnabled(boolean enable) { 665 if (mVerboseLoggingEnabled) Log.v(TAG, "setWatchdogTriggerEnabled: enable = " + enable); 666 // Reset failure counts before actives watchdog 667 if (enable) { 668 clearAllFailureCounts(); 669 } 670 mWatchdogAllowedToTrigger = enable; 671 } 672 673 /** 674 * Prints all networks & counts within mRecentAvailableNetworks to string 675 */ 676 @Override toString()677 public String toString() { 678 StringBuilder sb = new StringBuilder(); 679 sb.append("mWatchdogFeatureEnabled: ").append(getWifiWatchdogFeature()); 680 sb.append("\nmWatchdogAllowedToTrigger: ").append(mWatchdogAllowedToTrigger); 681 sb.append("\nmRecentAvailableNetworks: ").append(mRecentAvailableNetworks.size()); 682 for (Map.Entry<String, AvailableNetworkFailureCount> entry 683 : mRecentAvailableNetworks.entrySet()) { 684 sb.append("\n ").append(entry.getKey()).append(": ").append(entry.getValue()) 685 .append(", Age: ").append(entry.getValue().age); 686 } 687 sb.append("\nmSsidFailureCount:"); 688 for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry : 689 mSsidFailureCount.entrySet()) { 690 final AvailableNetworkFailureCount failureCount = entry.getValue().first; 691 final Integer apCount = entry.getValue().second; 692 sb.append("\n").append(entry.getKey()).append(": ").append(apCount).append(",") 693 .append(failureCount.toString()); 694 } 695 return sb.toString(); 696 } 697 698 /** 699 * @param bssid bssid to check the failures for 700 * @return true if sum of failure count is over FAILURE_THRESHOLD 701 */ 702 @VisibleForTesting isOverFailureThreshold(String bssid)703 boolean isOverFailureThreshold(String bssid) { 704 return (getFailureCount(bssid, FAILURE_CODE_ASSOCIATION) 705 + getFailureCount(bssid, FAILURE_CODE_AUTHENTICATION) 706 + getFailureCount(bssid, FAILURE_CODE_DHCP)) >= FAILURE_THRESHOLD; 707 } 708 709 /** 710 * Get the failure count for a specific bssid. This actually checks the ssid attached to the 711 * BSSID and returns the SSID count 712 * @param reason failure reason to get count for 713 */ 714 @VisibleForTesting getFailureCount(String bssid, int reason)715 int getFailureCount(String bssid, int reason) { 716 AvailableNetworkFailureCount availableNetworkFailureCount = 717 mRecentAvailableNetworks.get(bssid); 718 if (availableNetworkFailureCount == null) { 719 return 0; 720 } 721 String ssid = availableNetworkFailureCount.ssid; 722 Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid); 723 if (ssidFails == null) { 724 Log.d(TAG, "getFailureCount: Could not find SSID count for " + ssid); 725 return 0; 726 } 727 final AvailableNetworkFailureCount failCount = ssidFails.first; 728 switch (reason) { 729 case FAILURE_CODE_ASSOCIATION: 730 return failCount.associationRejection; 731 case FAILURE_CODE_AUTHENTICATION: 732 return failCount.authenticationFailure; 733 case FAILURE_CODE_DHCP: 734 return failCount.dhcpFailure; 735 default: 736 return 0; 737 } 738 } 739 740 /** 741 * Sets whether wifi watchdog should trigger recovery 742 */ setWifiWatchdogFeature(boolean enable)743 public void setWifiWatchdogFeature(boolean enable) { 744 logv("setWifiWatchdogFeature: " + enable); 745 mWatchdogFeatureEnabled = enable; 746 // for debugging purpose, reset mWatchdogAllowedToTrigger as well 747 setWatchdogTriggerEnabled(true); 748 } 749 750 /** 751 * Returns whether wifi watchdog should trigger recovery. 752 */ getWifiWatchdogFeature()753 public boolean getWifiWatchdogFeature() { 754 if (mWatchdogFeatureEnabled == null) { 755 mWatchdogFeatureEnabled = mContext.getResources().getBoolean( 756 R.bool.config_wifi_watchdog_enabled); 757 } 758 return mWatchdogFeatureEnabled; 759 } 760 761 /** Enable/disable verbose logging. */ enableVerboseLogging(int verbose)762 public void enableVerboseLogging(int verbose) { 763 mVerboseLoggingEnabled = verbose > 0; 764 } 765 766 @VisibleForTesting setBugReportProbability(double newProbability)767 void setBugReportProbability(double newProbability) { 768 mBugReportProbability = newProbability; 769 } 770 771 /** 772 * This class holds the failure counts for an 'available network' (one of the potential 773 * candidates for connection, as determined by framework). 774 */ 775 public static class AvailableNetworkFailureCount { 776 /** 777 * WifiConfiguration associated with this network. Can be null for Ephemeral networks 778 */ 779 public WifiConfiguration config; 780 /** 781 * SSID of the network (from ScanDetail) 782 */ 783 public String ssid = ""; 784 /** 785 * Number of times network has failed due to Association Rejection 786 */ 787 public int associationRejection = 0; 788 /** 789 * Number of times network has failed due to Authentication Failure or SSID_TEMP_DISABLED 790 */ 791 public int authenticationFailure = 0; 792 /** 793 * Number of times network has failed due to DHCP failure 794 */ 795 public int dhcpFailure = 0; 796 /** 797 * Number of scanResults since this network was last seen 798 */ 799 public int age = 0; 800 AvailableNetworkFailureCount(WifiConfiguration configParam)801 AvailableNetworkFailureCount(WifiConfiguration configParam) { 802 this.config = configParam; 803 } 804 805 /** 806 * @param reason failure reason to increment count for 807 */ incrementFailureCount(int reason)808 public void incrementFailureCount(int reason) { 809 switch (reason) { 810 case FAILURE_CODE_ASSOCIATION: 811 associationRejection++; 812 break; 813 case FAILURE_CODE_AUTHENTICATION: 814 authenticationFailure++; 815 break; 816 case FAILURE_CODE_DHCP: 817 dhcpFailure++; 818 break; 819 default: //do nothing 820 } 821 } 822 823 /** 824 * Set all failure counts for this network to 0 825 */ resetCounts()826 void resetCounts() { 827 associationRejection = 0; 828 authenticationFailure = 0; 829 dhcpFailure = 0; 830 } 831 toString()832 public String toString() { 833 return ssid + " HasEverConnected: " + ((config != null) 834 ? config.getNetworkSelectionStatus().hasEverConnected() : "null_config") 835 + ", Failures: {" 836 + "Assoc: " + associationRejection 837 + ", Auth: " + authenticationFailure 838 + ", Dhcp: " + dhcpFailure 839 + "}"; 840 } 841 } 842 843 /** 844 * Helper function for logging into local log buffer. 845 */ localLog(String s)846 private void localLog(String s) { 847 mLocalLog.log(s); 848 } 849 logv(String s)850 private void logv(String s) { 851 mLocalLog.log(s); 852 if (mVerboseLoggingEnabled) { 853 Log.v(TAG, s); 854 } 855 } 856 loge(String s)857 private void loge(String s) { 858 mLocalLog.log(s); 859 Log.e(TAG, s); 860 } 861 862 /** 863 * Dump the local log buffer and other internal state of WifiLastResortWatchdog. 864 */ dump(FileDescriptor fd, PrintWriter pw, String[] args)865 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 866 pw.println("Dump of WifiLastResortWatchdog"); 867 pw.println("WifiLastResortWatchdog - Log Begin ----"); 868 mLocalLog.dump(fd, pw, args); 869 pw.println("WifiLastResortWatchdog - Log End ----"); 870 } 871 } 872