content: /*
   * Copyright (C) 2016 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.wifi;
  
  import android.content.Context;
  import android.net.wifi.ScanResult;
  import android.net.wifi.WifiConfiguration;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.text.TextUtils;
  import android.util.LocalLog;
  import android.util.Log;
  import android.util.Pair;
  
  import com.android.internal.annotations.VisibleForTesting;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  
  /**
   * This Class is a Work-In-Progress, intended behavior is as follows:
   * Essentially this class automates a user toggling 'Airplane Mode' when WiFi "won't work".
   * IF each available saved network has failed connecting more times than the FAILURE_THRESHOLD
   * THEN Watchdog will restart Supplicant, wifi driver and return ClientModeImpl to InitialState.
   */
  public class WifiLastResortWatchdog {
      private static final String TAG = "WifiLastResortWatchdog";
      private boolean mVerboseLoggingEnabled = false;
      /**
       * Association Failure code
       */
      public static final int FAILURE_CODE_ASSOCIATION = 1;
      /**
       * Authentication Failure code
       */
      public static final int FAILURE_CODE_AUTHENTICATION = 2;
      /**
       * Dhcp Failure code
       */
      public static final int FAILURE_CODE_DHCP = 3;
      /**
       * Maximum number of scan results received since we last saw a BSSID.
       * If it is not seen before this limit is reached, the network is culled
       */
      public static final int MAX_BSSID_AGE = 10;
      /**
       * BSSID used to increment failure counts against ALL bssids associated with a particular SSID
       */
      public static final String BSSID_ANY = "any";
      /**
       * Failure count that each available networks must meet to possibly trigger the Watchdog
       */
      public static final int FAILURE_THRESHOLD = 7;
      public static final String BUGREPORT_TITLE = "Wifi watchdog triggered";
      public static final double PROB_TAKE_BUGREPORT_DEFAULT = 1;
  
      // Number of milliseconds to wait before re-enable Watchdog triger
      @VisibleForTesting
      public static final long LAST_TRIGGER_TIMEOUT_MILLIS = 2 * 3600 * 1000; // 2 hours
  
      private int mAbnormalConnectionDurationMs;
      private boolean mAbnormalConnectionBugreportEnabled;
  
  
      /**
       * Cached WifiConfigurations of available networks seen within MAX_BSSID_AGE scan results
       * Key:BSSID, Value:Counters of failure types
       */
      private Map<String, AvailableNetworkFailureCount> mRecentAvailableNetworks = new HashMap<>();
  
      /**
       * Map of SSID to <FailureCount, AP count>, used to count failures & number of access points
       * belonging to an SSID.
       */
      private Map<String, Pair<AvailableNetworkFailureCount, Integer>> mSsidFailureCount =
              new HashMap<>();
  
      // Tracks: if ClientModeImpl is in ConnectedState
      private boolean mWifiIsConnected = false;
      // Is Watchdog allowed to trigger now? Set to false after triggering. Set to true after
      // successfully connecting or a new network (SSID) becomes available to connect to.
      private boolean mWatchdogAllowedToTrigger = true;
      private long mTimeLastTrigger = 0;
      private String mSsidLastTrigger = null;
  
      private WifiInjector mWifiInjector;
      private WifiMetrics mWifiMetrics;
      private ClientModeImpl mClientModeImpl;
      private Looper mClientModeImplLooper;
      private double mBugReportProbability = PROB_TAKE_BUGREPORT_DEFAULT;
      private Clock mClock;
      private Context mContext;
      private DeviceConfigFacade mDeviceConfigFacade;
      // If any connection failure happened after watchdog triggering restart then assume watchdog
      // did not fix the problem
      private boolean mWatchdogFixedWifi = true;
      private long mLastStartConnectTime = 0;
      private Handler mHandler;
  
      /**
       * Local log used for debugging any WifiLastResortWatchdog issues.
       */
      private final LocalLog mLocalLog = new LocalLog(100);
  
      WifiLastResortWatchdog(WifiInjector wifiInjector, Context context, Clock clock,
              WifiMetrics wifiMetrics, ClientModeImpl clientModeImpl, Looper clientModeImplLooper,
              DeviceConfigFacade deviceConfigFacade) {
          mWifiInjector = wifiInjector;
          mClock = clock;
          mWifiMetrics = wifiMetrics;
          mClientModeImpl = clientModeImpl;
          mClientModeImplLooper = clientModeImplLooper;
          mContext = context;
          mDeviceConfigFacade = deviceConfigFacade;
          updateDeviceConfigFlags();
          mHandler = new Handler(clientModeImplLooper) {
              public void handleMessage(Message msg) {
                  processMessage(msg);
              }
          };
  
          mDeviceConfigFacade.addOnPropertiesChangedListener(
                  command -> mHandler.post(command),
                  properties -> {
                      updateDeviceConfigFlags();
                  });
      }
  
      private void updateDeviceConfigFlags() {
          mAbnormalConnectionBugreportEnabled =
                  mDeviceConfigFacade.isAbnormalConnectionBugreportEnabled();
          mAbnormalConnectionDurationMs =
                  mDeviceConfigFacade.getAbnormalConnectionDurationMs();
          logv("updateDeviceConfigFlags: mAbnormalConnectionDurationMs = "
                  + mAbnormalConnectionDurationMs
                  + ", mAbnormalConnectionBugreportEnabled = "
                  + mAbnormalConnectionBugreportEnabled);
      }
  
      /**
       * Returns handler for L2 events from supplicant.
       * @return Handler
       */
      public Handler getHandler() {
          return mHandler;
      }
  
      /**
       * Refreshes when the last CMD_START_CONNECT is triggered.
       */
      public void noteStartConnectTime() {
          mHandler.post(() -> {
              mLastStartConnectTime = mClock.getElapsedSinceBootMillis();
          });
      }
  
      private void processMessage(Message msg) {
          switch (msg.what) {
              case WifiMonitor.NETWORK_CONNECTION_EVENT:
                  // Trigger bugreport for successful connections that take abnormally long
                  if (mAbnormalConnectionBugreportEnabled && mLastStartConnectTime > 0) {
                      long durationMs = mClock.getElapsedSinceBootMillis() - mLastStartConnectTime;
                      if (durationMs > mAbnormalConnectionDurationMs) {
                          final String bugTitle = "Wi-Fi Bugreport: Abnormal connection time";
                          final String bugDetail = "Expected connection to take less than "
                                  + mAbnormalConnectionDurationMs + " milliseconds. "
                                  + "Actually took " + durationMs + " milliseconds.";
                          logv("Triggering bug report for abnormal connection time.");
                          mWifiInjector.getClientModeImplHandler().post(() -> {
                              mClientModeImpl.takeBugReport(bugTitle, bugDetail);
                          });
                      }
                  }
                  // Should reset last connection time after each connection regardless if bugreport
                  // is enabled or not.
                  mLastStartConnectTime = 0;
                  break;
              default:
                  return;
          }
      }
  
      /**
       * Refreshes recentAvailableNetworks with the latest available networks
       * Adds new networks, removes old ones that have timed out. Should be called after Wifi
       * framework decides what networks it is potentially connecting to.
       * @param availableNetworks ScanDetail & Config list of potential connection
       * candidates
       */
      public void updateAvailableNetworks(
              List<Pair<ScanDetail, WifiConfiguration>> availableNetworks) {
          if (mVerboseLoggingEnabled) {
              Log.v(TAG, "updateAvailableNetworks: size = " + availableNetworks.size());
          }
          // Add new networks to mRecentAvailableNetworks
          if (availableNetworks != null) {
              for (Pair<ScanDetail, WifiConfiguration> pair : availableNetworks) {
                  final ScanDetail scanDetail = pair.first;
                  final WifiConfiguration config = pair.second;
                  ScanResult scanResult = scanDetail.getScanResult();
                  if (scanResult == null) continue;
                  String bssid = scanResult.BSSID;
                  String ssid = "\"" + scanDetail.getSSID() + "\"";
                  if (mVerboseLoggingEnabled) {
                      Log.v(TAG, " " + bssid + ": " + scanDetail.getSSID());
                  }
                  // Cache the scanResult & WifiConfig
                  AvailableNetworkFailureCount availableNetworkFailureCount =
                          mRecentAvailableNetworks.get(bssid);
                  if (availableNetworkFailureCount == null) {
                      // New network is available
                      availableNetworkFailureCount = new AvailableNetworkFailureCount(config);
                      availableNetworkFailureCount.ssid = ssid;
  
                      // Count AP for this SSID
                      Pair<AvailableNetworkFailureCount, Integer> ssidFailsAndApCount =
                              mSsidFailureCount.get(ssid);
                      if (ssidFailsAndApCount == null) {
                          // This is a new SSID, create new FailureCount for it and set AP count to 1
                          ssidFailsAndApCount = Pair.create(new AvailableNetworkFailureCount(config),
                                  1);
                          // Do not re-enable Watchdog in LAST_TRIGGER_TIMEOUT_MILLIS
                          // after last time Watchdog be triggered
                          if (mTimeLastTrigger == 0
                                  || (mClock.getElapsedSinceBootMillis() - mTimeLastTrigger)
                                      >= LAST_TRIGGER_TIMEOUT_MILLIS) {
                              localLog("updateAvailableNetworks: setWatchdogTriggerEnabled to true");
                              setWatchdogTriggerEnabled(true);
                          }
                      } else {
                          final Integer numberOfAps = ssidFailsAndApCount.second;
                          // This is not a new SSID, increment the AP count for it
                          ssidFailsAndApCount = Pair.create(ssidFailsAndApCount.first,
                                  numberOfAps + 1);
                      }
                      mSsidFailureCount.put(ssid, ssidFailsAndApCount);
                  }
                  // refresh config if it is not null
                  if (config != null) {
                      availableNetworkFailureCount.config = config;
                  }
                  // If we saw a network, set its Age to -1 here, aging iteration will set it to 0
                  availableNetworkFailureCount.age = -1;
                  mRecentAvailableNetworks.put(bssid, availableNetworkFailureCount);
              }
          }
  
          // Iterate through available networks updating timeout counts & removing networks.
          Iterator<Map.Entry<String, AvailableNetworkFailureCount>> it =
                  mRecentAvailableNetworks.entrySet().iterator();
          while (it.hasNext()) {
              Map.Entry<String, AvailableNetworkFailureCount> entry = it.next();
              if (entry.getValue().age < MAX_BSSID_AGE - 1) {
                  entry.getValue().age++;
              } else {
                  // Decrement this SSID : AP count
                  String ssid = entry.getValue().ssid;
                  Pair<AvailableNetworkFailureCount, Integer> ssidFails =
                              mSsidFailureCount.get(ssid);
                  if (ssidFails != null) {
                      Integer apCount = ssidFails.second - 1;
                      if (apCount > 0) {
                          ssidFails = Pair.create(ssidFails.first, apCount);
                          mSsidFailureCount.put(ssid, ssidFails);
                      } else {
                          mSsidFailureCount.remove(ssid);
                      }
                  } else {
                      Log.d(TAG, "updateAvailableNetworks: SSID to AP count mismatch for " + ssid);
                  }
                  it.remove();
              }
          }
          if (mVerboseLoggingEnabled) Log.v(TAG, toString());
      }
  
      /**
       * Increments the failure reason count for the given bssid. Performs a check to see if we have
       * exceeded a failure threshold for all available networks, and executes the last resort restart
       * @param bssid of the network that has failed connection, can be "any"
       * @param reason Message id from ClientModeImpl for this failure
       * @return true if watchdog triggers, returned for test visibility
       */
      public boolean noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason) {
          if (mVerboseLoggingEnabled) {
              Log.v(TAG, "noteConnectionFailureAndTriggerIfNeeded: [" + ssid + ", " + bssid + ", "
                      + reason + "]");
          }
  
          // Update failure count for the failing network
          updateFailureCountForNetwork(ssid, bssid, reason);
  
          // If watchdog is not allowed to trigger it means a wifi restart is already triggered
          if (!mWatchdogAllowedToTrigger) {
              mWifiMetrics.incrementWatchdogTotalConnectionFailureCountAfterTrigger();
              mWatchdogFixedWifi = false;
          }
          // Have we met conditions to trigger the Watchdog Wifi restart?
          boolean isRestartNeeded = checkTriggerCondition();
          if (mVerboseLoggingEnabled) {
              Log.v(TAG, "isRestartNeeded = " + isRestartNeeded);
          }
          if (isRestartNeeded) {
              // Stop the watchdog from triggering until re-enabled
              localLog("noteConnectionFailureAndTriggerIfNeeded: setWatchdogTriggerEnabled to false");
              setWatchdogTriggerEnabled(false);
              mWatchdogFixedWifi = true;
              loge("Watchdog triggering recovery");
              mSsidLastTrigger = ssid;
              mTimeLastTrigger = mClock.getElapsedSinceBootMillis();
              localLog(toString());
              mWifiInjector.getSelfRecovery().trigger(SelfRecovery.REASON_LAST_RESORT_WATCHDOG);
              incrementWifiMetricsTriggerCounts();
              clearAllFailureCounts();
          }
          return isRestartNeeded;
      }
  
      /**
       * Handles transitions entering and exiting ClientModeImpl ConnectedState
       * Used to track wifistate, and perform watchdog count resetting
       * @param isEntering true if called from ConnectedState.enter(), false for exit()
       */
      public void connectedStateTransition(boolean isEntering) {
          logv("connectedStateTransition: isEntering = " + isEntering);
  
          mWifiIsConnected = isEntering;
          if (!isEntering) {
              return;
          }
          if (!mWatchdogAllowedToTrigger && mWatchdogFixedWifi
                  && checkIfAtleastOneNetworkHasEverConnected()
                  && checkIfConnectedBackToSameSsid()) {
              takeBugReportWithCurrentProbability("Wifi fixed after restart");
              // WiFi has connected after a Watchdog trigger, without any new networks becoming
              // available, log a Watchdog success in wifi metrics
              mWifiMetrics.incrementNumLastResortWatchdogSuccesses();
              long durationMs = mClock.getElapsedSinceBootMillis() - mTimeLastTrigger;
              mWifiMetrics.setWatchdogSuccessTimeDurationMs(durationMs);
          }
          // We connected to something! Reset failure counts for everything
          clearAllFailureCounts();
          // If the watchdog trigger was disabled (it triggered), connecting means we did
          // something right, re-enable it so it can fire again.
          localLog("connectedStateTransition: setWatchdogTriggerEnabled to true");
          setWatchdogTriggerEnabled(true);
      }
  
      /**
       * Helper function to check if device connect back to same
       * SSID after watchdog trigger
       */
      private boolean checkIfConnectedBackToSameSsid() {
          if (TextUtils.equals(mSsidLastTrigger, mClientModeImpl.getWifiInfo().getSSID())) {
              return true;
          }
          localLog("checkIfConnectedBackToSameSsid: different SSID be connected");
          return false;
      }
  
      /**
       * Triggers a wifi specific bugreport with a based on the current trigger probability.
       * @param bugDetail description of the bug
       */
      private void takeBugReportWithCurrentProbability(String bugDetail) {
          if (mBugReportProbability <= Math.random()) {
              return;
          }
          (new Handler(mClientModeImplLooper)).post(() -> {
              mClientModeImpl.takeBugReport(BUGREPORT_TITLE, bugDetail);
          });
      }
  
      /**
       * Increments the failure reason count for the given network, in 'mSsidFailureCount'
       * Failures are counted per SSID, either; by using the ssid string when the bssid is "any"
       * or by looking up the ssid attached to a specific bssid
       * An unused set of counts is also kept which is bssid specific, in 'mRecentAvailableNetworks'
       * @param ssid of the network that has failed connection
       * @param bssid of the network that has failed connection, can be "any"
       * @param reason Message id from ClientModeImpl for this failure
       */
      private void updateFailureCountForNetwork(String ssid, String bssid, int reason) {
          logv("updateFailureCountForNetwork: [" + ssid + ", " + bssid + ", "
                  + reason + "]");
          if (BSSID_ANY.equals(bssid)) {
              incrementSsidFailureCount(ssid, reason);
          } else {
              // Bssid count is actually unused except for logging purposes
              // SSID count is incremented within the BSSID counting method
              incrementBssidFailureCount(ssid, bssid, reason);
          }
      }
  
      /**
       * Update the per-SSID failure count
       * @param ssid the ssid to increment failure count for
       * @param reason the failure type to increment count for
       */
      private void incrementSsidFailureCount(String ssid, int reason) {
          Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
          if (ssidFails == null) {
              Log.d(TAG, "updateFailureCountForNetwork: No networks for ssid = " + ssid);
              return;
          }
          AvailableNetworkFailureCount failureCount = ssidFails.first;
          failureCount.incrementFailureCount(reason);
      }
  
      /**
       * Update the per-BSSID failure count
       * @param bssid the bssid to increment failure count for
       * @param reason the failure type to increment count for
       */
      private void incrementBssidFailureCount(String ssid, String bssid, int reason) {
          AvailableNetworkFailureCount availableNetworkFailureCount =
                  mRecentAvailableNetworks.get(bssid);
          if (availableNetworkFailureCount == null) {
              Log.d(TAG, "updateFailureCountForNetwork: Unable to find Network [" + ssid
                      + ", " + bssid + "]");
              return;
          }
          if (!availableNetworkFailureCount.ssid.equals(ssid)) {
              Log.d(TAG, "updateFailureCountForNetwork: Failed connection attempt has"
                      + " wrong ssid. Failed [" + ssid + ", " + bssid + "], buffered ["
                      + availableNetworkFailureCount.ssid + ", " + bssid + "]");
              return;
          }
          if (availableNetworkFailureCount.config == null) {
              if (mVerboseLoggingEnabled) {
                  Log.v(TAG, "updateFailureCountForNetwork: network has no config ["
                          + ssid + ", " + bssid + "]");
              }
          }
          availableNetworkFailureCount.incrementFailureCount(reason);
          incrementSsidFailureCount(ssid, reason);
      }
  
      /**
       * Helper function to check if we should ignore BSSID update.
       * @param bssid BSSID of the access point
       * @return true if we should ignore BSSID update
       */
      public boolean shouldIgnoreBssidUpdate(String bssid) {
          return mWatchdogAllowedToTrigger
                  && isBssidOnlyApOfSsid(bssid)
                  && isSingleSsidRecorded()
                  && checkIfAtleastOneNetworkHasEverConnected();
      }
  
      /**
       * Helper function to check if we should ignore SSID update.
       * @return true if should ignore SSID update
       */
      public boolean shouldIgnoreSsidUpdate() {
          return mWatchdogAllowedToTrigger
                  && isSingleSsidRecorded()
                  && checkIfAtleastOneNetworkHasEverConnected();
      }
  
      /**
       * Check the specified BSSID is the only BSSID for its corresponding SSID.
       * @param bssid BSSID of the access point
       * @return true if only BSSID for its corresponding SSID be observed
       */
      private boolean isBssidOnlyApOfSsid(String bssid) {
          AvailableNetworkFailureCount availableNetworkFailureCount =
                  mRecentAvailableNetworks.get(bssid);
          if (availableNetworkFailureCount == null) {
              return false;
          }
          String ssid = availableNetworkFailureCount.ssid;
          Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
          if (ssidFails == null) {
              Log.d(TAG, "isOnlyBssidAvailable: Could not find SSID count for " + ssid);
              return false;
          }
          if (ssidFails.second != 1) {
              return false;
          }
          return true;
      }
  
      /**
       * Check there is only single SSID be observed.
       * @return true if only single SSID be observed.
       */
      private boolean isSingleSsidRecorded() {
          return (mSsidFailureCount.size() == 1);
      }
  
      /**
       * Check trigger condition: For all available networks, have we met a failure threshold for each
       * of them, and have previously connected to at-least one of the available networks
       * @return is the trigger condition true
       */
      private boolean checkTriggerCondition() {
          if (mVerboseLoggingEnabled) Log.v(TAG, "checkTriggerCondition.");
          // Don't check Watchdog trigger if wifi is in a connected state
          // (This should not occur, but we want to protect against any race conditions)
          if (mWifiIsConnected) return false;
          // Don't check Watchdog trigger if trigger is not enabled
          if (!mWatchdogAllowedToTrigger) return false;
  
          for (Map.Entry<String, AvailableNetworkFailureCount> entry
                  : mRecentAvailableNetworks.entrySet()) {
              if (!isOverFailureThreshold(entry.getKey())) {
                  // This available network is not over failure threshold, meaning we still have a
                  // network to try connecting to
                  return false;
              }
          }
          // We have met the failure count for every available network.
          // Trigger restart if there exists at-least one network that we have previously connected.
          boolean atleastOneNetworkHasEverConnected = checkIfAtleastOneNetworkHasEverConnected();
          logv("checkTriggerCondition: return = " + atleastOneNetworkHasEverConnected);
          return checkIfAtleastOneNetworkHasEverConnected();
      }
  
      private boolean checkIfAtleastOneNetworkHasEverConnected() {
          for (Map.Entry<String, AvailableNetworkFailureCount> entry
                  : mRecentAvailableNetworks.entrySet()) {
              if (entry.getValue().config != null
                      && entry.getValue().config.getNetworkSelectionStatus().getHasEverConnected()) {
                  return true;
              }
          }
          return false;
      }
  
      /**
       * Update WifiMetrics with various Watchdog stats (trigger counts, failed network counts)
       */
      private void incrementWifiMetricsTriggerCounts() {
          if (mVerboseLoggingEnabled) Log.v(TAG, "incrementWifiMetricsTriggerCounts.");
          mWifiMetrics.incrementNumLastResortWatchdogTriggers();
          mWifiMetrics.addCountToNumLastResortWatchdogAvailableNetworksTotal(
                  mSsidFailureCount.size());
          // Number of networks over each failure type threshold, present at trigger time
          int badAuth = 0;
          int badAssoc = 0;
          int badDhcp = 0;
          int badSum = 0;
          for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry
                  : mSsidFailureCount.entrySet()) {
              badSum = entry.getValue().first.associationRejection
                      + entry.getValue().first.authenticationFailure
                      + entry.getValue().first.dhcpFailure;
              // count as contributor if over half of badSum.
              if (badSum >= FAILURE_THRESHOLD) {
                  badAssoc += (entry.getValue().first.associationRejection >= badSum / 2) ? 1 : 0;
                  badAuth += (entry.getValue().first.authenticationFailure >= badSum / 2) ? 1 : 0;
                  badDhcp += (entry.getValue().first.dhcpFailure >= badSum / 2) ? 1 : 0;
              }
          }
          if (badAuth > 0) {
              mWifiMetrics.addCountToNumLastResortWatchdogBadAuthenticationNetworksTotal(badAuth);
              mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAuthentication();
          }
          if (badAssoc > 0) {
              mWifiMetrics.addCountToNumLastResortWatchdogBadAssociationNetworksTotal(badAssoc);
              mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAssociation();
          }
          if (badDhcp > 0) {
              mWifiMetrics.addCountToNumLastResortWatchdogBadDhcpNetworksTotal(badDhcp);
              mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadDhcp();
          }
      }
  
      /**
       * Clear failure counts for each network in recentAvailableNetworks
       */
      public void clearAllFailureCounts() {
          if (mVerboseLoggingEnabled) Log.v(TAG, "clearAllFailureCounts.");
          for (Map.Entry<String, AvailableNetworkFailureCount> entry
                  : mRecentAvailableNetworks.entrySet()) {
              final AvailableNetworkFailureCount failureCount = entry.getValue();
              failureCount.resetCounts();
          }
          for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry
                  : mSsidFailureCount.entrySet()) {
              final AvailableNetworkFailureCount failureCount = entry.getValue().first;
              failureCount.resetCounts();
          }
      }
      /**
       * Gets the buffer of recently available networks
       */
      Map<String, AvailableNetworkFailureCount> getRecentAvailableNetworks() {
          return mRecentAvailableNetworks;
      }
  
      /**
       * Activates or deactivates the Watchdog trigger. Counting and network buffering still occurs
       * @param enable true to enable the Watchdog trigger, false to disable it
       */
      private void setWatchdogTriggerEnabled(boolean enable) {
          if (mVerboseLoggingEnabled) Log.v(TAG, "setWatchdogTriggerEnabled: enable = " + enable);
          mWatchdogAllowedToTrigger = enable;
      }
  
      /**
       * Prints all networks & counts within mRecentAvailableNetworks to string
       */
      public String toString() {
          StringBuilder sb = new StringBuilder();
          sb.append("mWatchdogAllowedToTrigger: ").append(mWatchdogAllowedToTrigger);
          sb.append("\nmWifiIsConnected: ").append(mWifiIsConnected);
          sb.append("\nmRecentAvailableNetworks: ").append(mRecentAvailableNetworks.size());
          for (Map.Entry<String, AvailableNetworkFailureCount> entry
                  : mRecentAvailableNetworks.entrySet()) {
              sb.append("\n ").append(entry.getKey()).append(": ").append(entry.getValue())
                  .append(", Age: ").append(entry.getValue().age);
          }
          sb.append("\nmSsidFailureCount:");
          for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry :
                  mSsidFailureCount.entrySet()) {
              final AvailableNetworkFailureCount failureCount = entry.getValue().first;
              final Integer apCount = entry.getValue().second;
              sb.append("\n").append(entry.getKey()).append(": ").append(apCount).append(",")
                      .append(failureCount.toString());
          }
          return sb.toString();
      }
  
      /**
       * @param bssid bssid to check the failures for
       * @return true if sum of failure count is over FAILURE_THRESHOLD
       */
      public boolean isOverFailureThreshold(String bssid) {
          return (getFailureCount(bssid, FAILURE_CODE_ASSOCIATION)
                  + getFailureCount(bssid, FAILURE_CODE_AUTHENTICATION)
                  + getFailureCount(bssid, FAILURE_CODE_DHCP)) >= FAILURE_THRESHOLD;
      }
  
      /**
       * Get the failure count for a specific bssid. This actually checks the ssid attached to the
       * BSSID and returns the SSID count
       * @param reason failure reason to get count for
       */
      public int getFailureCount(String bssid, int reason) {
          AvailableNetworkFailureCount availableNetworkFailureCount =
                  mRecentAvailableNetworks.get(bssid);
          if (availableNetworkFailureCount == null) {
              return 0;
          }
          String ssid = availableNetworkFailureCount.ssid;
          Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
          if (ssidFails == null) {
              Log.d(TAG, "getFailureCount: Could not find SSID count for " + ssid);
              return 0;
          }
          final AvailableNetworkFailureCount failCount = ssidFails.first;
          switch (reason) {
              case FAILURE_CODE_ASSOCIATION:
                  return failCount.associationRejection;
              case FAILURE_CODE_AUTHENTICATION:
                  return failCount.authenticationFailure;
              case FAILURE_CODE_DHCP:
                  return failCount.dhcpFailure;
              default:
                  return 0;
          }
      }
  
      protected void enableVerboseLogging(int verbose) {
          if (verbose > 0) {
              mVerboseLoggingEnabled = true;
          } else {
              mVerboseLoggingEnabled = false;
          }
      }
  
      @VisibleForTesting
      protected void setBugReportProbability(double newProbability) {
          mBugReportProbability = newProbability;
      }
  
      /**
       * This class holds the failure counts for an 'available network' (one of the potential
       * candidates for connection, as determined by framework).
       */
      public static class AvailableNetworkFailureCount {
          /**
           * WifiConfiguration associated with this network. Can be null for Ephemeral networks
           */
          public WifiConfiguration config;
          /**
          * SSID of the network (from ScanDetail)
          */
          public String ssid = "";
          /**
           * Number of times network has failed due to Association Rejection
           */
          public int associationRejection = 0;
          /**
           * Number of times network has failed due to Authentication Failure or SSID_TEMP_DISABLED
           */
          public int authenticationFailure = 0;
          /**
           * Number of times network has failed due to DHCP failure
           */
          public int dhcpFailure = 0;
          /**
           * Number of scanResults since this network was last seen
           */
          public int age = 0;
  
          AvailableNetworkFailureCount(WifiConfiguration configParam) {
              this.config = configParam;
          }
  
          /**
           * @param reason failure reason to increment count for
           */
          public void incrementFailureCount(int reason) {
              switch (reason) {
                  case FAILURE_CODE_ASSOCIATION:
                      associationRejection++;
                      break;
                  case FAILURE_CODE_AUTHENTICATION:
                      authenticationFailure++;
                      break;
                  case FAILURE_CODE_DHCP:
                      dhcpFailure++;
                      break;
                  default: //do nothing
              }
          }
  
          /**
           * Set all failure counts for this network to 0
           */
          void resetCounts() {
              associationRejection = 0;
              authenticationFailure = 0;
              dhcpFailure = 0;
          }
  
          public String toString() {
              return  ssid + " HasEverConnected: " + ((config != null)
                      ? config.getNetworkSelectionStatus().getHasEverConnected() : "null_config")
                      + ", Failures: {"
                      + "Assoc: " + associationRejection
                      + ", Auth: " + authenticationFailure
                      + ", Dhcp: " + dhcpFailure
                      + "}";
          }
      }
  
      /**
       * Helper function for logging into local log buffer.
       */
      private void localLog(String s) {
          mLocalLog.log(s);
      }
  
      private void logv(String s) {
          mLocalLog.log(s);
          if (mVerboseLoggingEnabled) {
              Log.v(TAG, s);
          }
      }
  
      private void loge(String s) {
          mLocalLog.log(s);
          Log.e(TAG, s);
      }
  
      /**
       * Dump the local log buffer and other internal state of WifiLastResortWatchdog.
       */
      public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
          pw.println("Dump of WifiLastResortWatchdog");
          pw.println("WifiLastResortWatchdog - Log Begin ----");
          mLocalLog.dump(fd, pw, args);
          pw.println("WifiLastResortWatchdog - Log End ----");
      }
  }
  