Home | History | Annotate | Download | only in wifi
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server.wifi;
     18 
     19 import android.net.wifi.ScanResult;
     20 import android.net.wifi.WifiConfiguration;
     21 import android.util.Log;
     22 import android.util.Pair;
     23 
     24 import java.util.HashMap;
     25 import java.util.Iterator;
     26 import java.util.List;
     27 import java.util.Map;
     28 
     29 /**
     30  * This Class is a Work-In-Progress, intended behavior is as follows:
     31  * Essentially this class automates a user toggling 'Airplane Mode' when WiFi "won't work".
     32  * IF each available saved network has failed connecting more times than the FAILURE_THRESHOLD
     33  * THEN Watchdog will restart Supplicant, wifi driver and return WifiStateMachine to InitialState.
     34  */
     35 public class WifiLastResortWatchdog {
     36     private static final String TAG = "WifiLastResortWatchdog";
     37     private boolean mVerboseLoggingEnabled = false;
     38     /**
     39      * Association Failure code
     40      */
     41     public static final int FAILURE_CODE_ASSOCIATION = 1;
     42     /**
     43      * Authentication Failure code
     44      */
     45     public static final int FAILURE_CODE_AUTHENTICATION = 2;
     46     /**
     47      * Dhcp Failure code
     48      */
     49     public static final int FAILURE_CODE_DHCP = 3;
     50     /**
     51      * Maximum number of scan results received since we last saw a BSSID.
     52      * If it is not seen before this limit is reached, the network is culled
     53      */
     54     public static final int MAX_BSSID_AGE = 10;
     55     /**
     56      * BSSID used to increment failure counts against ALL bssids associated with a particular SSID
     57      */
     58     public static final String BSSID_ANY = "any";
     59     /**
     60      * Failure count that each available networks must meet to possibly trigger the Watchdog
     61      */
     62     public static final int FAILURE_THRESHOLD = 7;
     63     /**
     64      * Cached WifiConfigurations of available networks seen within MAX_BSSID_AGE scan results
     65      * Key:BSSID, Value:Counters of failure types
     66      */
     67     private Map<String, AvailableNetworkFailureCount> mRecentAvailableNetworks = new HashMap<>();
     68     /**
     69      * Map of SSID to <FailureCount, AP count>, used to count failures & number of access points
     70      * belonging to an SSID.
     71      */
     72     private Map<String, Pair<AvailableNetworkFailureCount, Integer>> mSsidFailureCount =
     73             new HashMap<>();
     74     // Tracks: if WifiStateMachine is in ConnectedState
     75     private boolean mWifiIsConnected = false;
     76     // Is Watchdog allowed to trigger now? Set to false after triggering. Set to true after
     77     // successfully connecting or a new network (SSID) becomes available to connect to.
     78     private boolean mWatchdogAllowedToTrigger = true;
     79 
     80     private SelfRecovery mSelfRecovery;
     81     private WifiMetrics mWifiMetrics;
     82 
     83     WifiLastResortWatchdog(SelfRecovery selfRecovery, WifiMetrics wifiMetrics) {
     84         mSelfRecovery = selfRecovery;
     85         mWifiMetrics = wifiMetrics;
     86     }
     87 
     88     /**
     89      * Refreshes recentAvailableNetworks with the latest available networks
     90      * Adds new networks, removes old ones that have timed out. Should be called after Wifi
     91      * framework decides what networks it is potentially connecting to.
     92      * @param availableNetworks ScanDetail & Config list of potential connection
     93      * candidates
     94      */
     95     public void updateAvailableNetworks(
     96             List<Pair<ScanDetail, WifiConfiguration>> availableNetworks) {
     97         if (mVerboseLoggingEnabled) {
     98             Log.v(TAG, "updateAvailableNetworks: size = " + availableNetworks.size());
     99         }
    100         // Add new networks to mRecentAvailableNetworks
    101         if (availableNetworks != null) {
    102             for (Pair<ScanDetail, WifiConfiguration> pair : availableNetworks) {
    103                 final ScanDetail scanDetail = pair.first;
    104                 final WifiConfiguration config = pair.second;
    105                 ScanResult scanResult = scanDetail.getScanResult();
    106                 if (scanResult == null) continue;
    107                 String bssid = scanResult.BSSID;
    108                 String ssid = "\"" + scanDetail.getSSID() + "\"";
    109                 if (mVerboseLoggingEnabled) {
    110                     Log.v(TAG, " " + bssid + ": " + scanDetail.getSSID());
    111                 }
    112                 // Cache the scanResult & WifiConfig
    113                 AvailableNetworkFailureCount availableNetworkFailureCount =
    114                         mRecentAvailableNetworks.get(bssid);
    115                 if (availableNetworkFailureCount == null) {
    116                     // New network is available
    117                     availableNetworkFailureCount = new AvailableNetworkFailureCount(config);
    118                     availableNetworkFailureCount.ssid = ssid;
    119 
    120                     // Count AP for this SSID
    121                     Pair<AvailableNetworkFailureCount, Integer> ssidFailsAndApCount =
    122                             mSsidFailureCount.get(ssid);
    123                     if (ssidFailsAndApCount == null) {
    124                         // This is a new SSID, create new FailureCount for it and set AP count to 1
    125                         ssidFailsAndApCount = Pair.create(new AvailableNetworkFailureCount(config),
    126                                 1);
    127                         setWatchdogTriggerEnabled(true);
    128                     } else {
    129                         final Integer numberOfAps = ssidFailsAndApCount.second;
    130                         // This is not a new SSID, increment the AP count for it
    131                         ssidFailsAndApCount = Pair.create(ssidFailsAndApCount.first,
    132                                 numberOfAps + 1);
    133                     }
    134                     mSsidFailureCount.put(ssid, ssidFailsAndApCount);
    135                 }
    136                 // refresh config if it is not null
    137                 if (config != null) {
    138                     availableNetworkFailureCount.config = config;
    139                 }
    140                 // If we saw a network, set its Age to -1 here, aging iteration will set it to 0
    141                 availableNetworkFailureCount.age = -1;
    142                 mRecentAvailableNetworks.put(bssid, availableNetworkFailureCount);
    143             }
    144         }
    145 
    146         // Iterate through available networks updating timeout counts & removing networks.
    147         Iterator<Map.Entry<String, AvailableNetworkFailureCount>> it =
    148                 mRecentAvailableNetworks.entrySet().iterator();
    149         while (it.hasNext()) {
    150             Map.Entry<String, AvailableNetworkFailureCount> entry = it.next();
    151             if (entry.getValue().age < MAX_BSSID_AGE - 1) {
    152                 entry.getValue().age++;
    153             } else {
    154                 // Decrement this SSID : AP count
    155                 String ssid = entry.getValue().ssid;
    156                 Pair<AvailableNetworkFailureCount, Integer> ssidFails =
    157                             mSsidFailureCount.get(ssid);
    158                 if (ssidFails != null) {
    159                     Integer apCount = ssidFails.second - 1;
    160                     if (apCount > 0) {
    161                         ssidFails = Pair.create(ssidFails.first, apCount);
    162                         mSsidFailureCount.put(ssid, ssidFails);
    163                     } else {
    164                         mSsidFailureCount.remove(ssid);
    165                     }
    166                 } else {
    167                     Log.d(TAG, "updateAvailableNetworks: SSID to AP count mismatch for " + ssid);
    168                 }
    169                 it.remove();
    170             }
    171         }
    172         if (mVerboseLoggingEnabled) Log.v(TAG, toString());
    173     }
    174 
    175     /**
    176      * Increments the failure reason count for the given bssid. Performs a check to see if we have
    177      * exceeded a failure threshold for all available networks, and executes the last resort restart
    178      * @param bssid of the network that has failed connection, can be "any"
    179      * @param reason Message id from WifiStateMachine for this failure
    180      * @return true if watchdog triggers, returned for test visibility
    181      */
    182     public boolean noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason) {
    183         if (mVerboseLoggingEnabled) {
    184             Log.v(TAG, "noteConnectionFailureAndTriggerIfNeeded: [" + ssid + ", " + bssid + ", "
    185                     + reason + "]");
    186         }
    187         // Update failure count for the failing network
    188         updateFailureCountForNetwork(ssid, bssid, reason);
    189 
    190         // Have we met conditions to trigger the Watchdog Wifi restart?
    191         boolean isRestartNeeded = checkTriggerCondition();
    192         if (mVerboseLoggingEnabled) {
    193             Log.v(TAG, "isRestartNeeded = " + isRestartNeeded);
    194         }
    195         if (isRestartNeeded) {
    196             // Stop the watchdog from triggering until re-enabled
    197             setWatchdogTriggerEnabled(false);
    198             Log.e(TAG, "Watchdog triggering recovery");
    199             mSelfRecovery.trigger(SelfRecovery.REASON_LAST_RESORT_WATCHDOG);
    200             // increment various watchdog trigger count stats
    201             incrementWifiMetricsTriggerCounts();
    202             clearAllFailureCounts();
    203         }
    204         return isRestartNeeded;
    205     }
    206 
    207     /**
    208      * Handles transitions entering and exiting WifiStateMachine ConnectedState
    209      * Used to track wifistate, and perform watchdog count reseting
    210      * @param isEntering true if called from ConnectedState.enter(), false for exit()
    211      */
    212     public void connectedStateTransition(boolean isEntering) {
    213         if (mVerboseLoggingEnabled) {
    214             Log.v(TAG, "connectedStateTransition: isEntering = " + isEntering);
    215         }
    216         mWifiIsConnected = isEntering;
    217 
    218         if (!mWatchdogAllowedToTrigger) {
    219             // WiFi has connected after a Watchdog trigger, without any new networks becoming
    220             // available, log a Watchdog success in wifi metrics
    221             mWifiMetrics.incrementNumLastResortWatchdogSuccesses();
    222         }
    223         if (isEntering) {
    224             // We connected to something! Reset failure counts for everything
    225             clearAllFailureCounts();
    226             // If the watchdog trigger was disabled (it triggered), connecting means we did
    227             // something right, re-enable it so it can fire again.
    228             setWatchdogTriggerEnabled(true);
    229         }
    230     }
    231 
    232     /**
    233      * Increments the failure reason count for the given network, in 'mSsidFailureCount'
    234      * Failures are counted per SSID, either; by using the ssid string when the bssid is "any"
    235      * or by looking up the ssid attached to a specific bssid
    236      * An unused set of counts is also kept which is bssid specific, in 'mRecentAvailableNetworks'
    237      * @param ssid of the network that has failed connection
    238      * @param bssid of the network that has failed connection, can be "any"
    239      * @param reason Message id from WifiStateMachine for this failure
    240      */
    241     private void updateFailureCountForNetwork(String ssid, String bssid, int reason) {
    242         if (mVerboseLoggingEnabled) {
    243             Log.v(TAG, "updateFailureCountForNetwork: [" + ssid + ", " + bssid + ", "
    244                     + reason + "]");
    245         }
    246         if (BSSID_ANY.equals(bssid)) {
    247             incrementSsidFailureCount(ssid, reason);
    248         } else {
    249             // Bssid count is actually unused except for logging purposes
    250             // SSID count is incremented within the BSSID counting method
    251             incrementBssidFailureCount(ssid, bssid, reason);
    252         }
    253     }
    254 
    255     /**
    256      * Update the per-SSID failure count
    257      * @param ssid the ssid to increment failure count for
    258      * @param reason the failure type to increment count for
    259      */
    260     private void incrementSsidFailureCount(String ssid, int reason) {
    261         Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
    262         if (ssidFails == null) {
    263             Log.d(TAG, "updateFailureCountForNetwork: No networks for ssid = " + ssid);
    264             return;
    265         }
    266         AvailableNetworkFailureCount failureCount = ssidFails.first;
    267         failureCount.incrementFailureCount(reason);
    268     }
    269 
    270     /**
    271      * Update the per-BSSID failure count
    272      * @param bssid the bssid to increment failure count for
    273      * @param reason the failure type to increment count for
    274      */
    275     private void incrementBssidFailureCount(String ssid, String bssid, int reason) {
    276         AvailableNetworkFailureCount availableNetworkFailureCount =
    277                 mRecentAvailableNetworks.get(bssid);
    278         if (availableNetworkFailureCount == null) {
    279             Log.d(TAG, "updateFailureCountForNetwork: Unable to find Network [" + ssid
    280                     + ", " + bssid + "]");
    281             return;
    282         }
    283         if (!availableNetworkFailureCount.ssid.equals(ssid)) {
    284             Log.d(TAG, "updateFailureCountForNetwork: Failed connection attempt has"
    285                     + " wrong ssid. Failed [" + ssid + ", " + bssid + "], buffered ["
    286                     + availableNetworkFailureCount.ssid + ", " + bssid + "]");
    287             return;
    288         }
    289         if (availableNetworkFailureCount.config == null) {
    290             if (mVerboseLoggingEnabled) {
    291                 Log.v(TAG, "updateFailureCountForNetwork: network has no config ["
    292                         + ssid + ", " + bssid + "]");
    293             }
    294         }
    295         availableNetworkFailureCount.incrementFailureCount(reason);
    296         incrementSsidFailureCount(ssid, reason);
    297     }
    298 
    299     /**
    300      * Check trigger condition: For all available networks, have we met a failure threshold for each
    301      * of them, and have previously connected to at-least one of the available networks
    302      * @return is the trigger condition true
    303      */
    304     private boolean checkTriggerCondition() {
    305         if (mVerboseLoggingEnabled) Log.v(TAG, "checkTriggerCondition.");
    306         // Don't check Watchdog trigger if wifi is in a connected state
    307         // (This should not occur, but we want to protect against any race conditions)
    308         if (mWifiIsConnected) return false;
    309         // Don't check Watchdog trigger if trigger is not enabled
    310         if (!mWatchdogAllowedToTrigger) return false;
    311 
    312         boolean atleastOneNetworkHasEverConnected = false;
    313         for (Map.Entry<String, AvailableNetworkFailureCount> entry
    314                 : mRecentAvailableNetworks.entrySet()) {
    315             if (entry.getValue().config != null
    316                     && entry.getValue().config.getNetworkSelectionStatus().getHasEverConnected()) {
    317                 atleastOneNetworkHasEverConnected = true;
    318             }
    319             if (!isOverFailureThreshold(entry.getKey())) {
    320                 // This available network is not over failure threshold, meaning we still have a
    321                 // network to try connecting to
    322                 return false;
    323             }
    324         }
    325         // We have met the failure count for every available network & there is at-least one network
    326         // we have previously connected to present.
    327         if (mVerboseLoggingEnabled) {
    328             Log.v(TAG, "checkTriggerCondition: return = " + atleastOneNetworkHasEverConnected);
    329         }
    330         return atleastOneNetworkHasEverConnected;
    331     }
    332 
    333     /**
    334      * Update WifiMetrics with various Watchdog stats (trigger counts, failed network counts)
    335      */
    336     private void incrementWifiMetricsTriggerCounts() {
    337         if (mVerboseLoggingEnabled) Log.v(TAG, "incrementWifiMetricsTriggerCounts.");
    338         mWifiMetrics.incrementNumLastResortWatchdogTriggers();
    339         mWifiMetrics.addCountToNumLastResortWatchdogAvailableNetworksTotal(
    340                 mSsidFailureCount.size());
    341         // Number of networks over each failure type threshold, present at trigger time
    342         int badAuth = 0;
    343         int badAssoc = 0;
    344         int badDhcp = 0;
    345         for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry
    346                 : mSsidFailureCount.entrySet()) {
    347             badAuth += (entry.getValue().first.authenticationFailure >= FAILURE_THRESHOLD) ? 1 : 0;
    348             badAssoc += (entry.getValue().first.associationRejection >= FAILURE_THRESHOLD) ? 1 : 0;
    349             badDhcp += (entry.getValue().first.dhcpFailure >= FAILURE_THRESHOLD) ? 1 : 0;
    350         }
    351         if (badAuth > 0) {
    352             mWifiMetrics.addCountToNumLastResortWatchdogBadAuthenticationNetworksTotal(badAuth);
    353             mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAuthentication();
    354         }
    355         if (badAssoc > 0) {
    356             mWifiMetrics.addCountToNumLastResortWatchdogBadAssociationNetworksTotal(badAssoc);
    357             mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAssociation();
    358         }
    359         if (badDhcp > 0) {
    360             mWifiMetrics.addCountToNumLastResortWatchdogBadDhcpNetworksTotal(badDhcp);
    361             mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadDhcp();
    362         }
    363     }
    364 
    365     /**
    366      * Clear failure counts for each network in recentAvailableNetworks
    367      */
    368     private void clearAllFailureCounts() {
    369         if (mVerboseLoggingEnabled) Log.v(TAG, "clearAllFailureCounts.");
    370         for (Map.Entry<String, AvailableNetworkFailureCount> entry
    371                 : mRecentAvailableNetworks.entrySet()) {
    372             final AvailableNetworkFailureCount failureCount = entry.getValue();
    373             entry.getValue().resetCounts();
    374         }
    375         for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry
    376                 : mSsidFailureCount.entrySet()) {
    377             final AvailableNetworkFailureCount failureCount = entry.getValue().first;
    378             failureCount.resetCounts();
    379         }
    380     }
    381     /**
    382      * Gets the buffer of recently available networks
    383      */
    384     Map<String, AvailableNetworkFailureCount> getRecentAvailableNetworks() {
    385         return mRecentAvailableNetworks;
    386     }
    387 
    388     /**
    389      * Activates or deactivates the Watchdog trigger. Counting and network buffering still occurs
    390      * @param enable true to enable the Watchdog trigger, false to disable it
    391      */
    392     private void setWatchdogTriggerEnabled(boolean enable) {
    393         if (mVerboseLoggingEnabled) Log.v(TAG, "setWatchdogTriggerEnabled: enable = " + enable);
    394         mWatchdogAllowedToTrigger = enable;
    395     }
    396 
    397     /**
    398      * Prints all networks & counts within mRecentAvailableNetworks to string
    399      */
    400     public String toString() {
    401         StringBuilder sb = new StringBuilder();
    402         sb.append("mWatchdogAllowedToTrigger: ").append(mWatchdogAllowedToTrigger);
    403         sb.append("\nmWifiIsConnected: ").append(mWifiIsConnected);
    404         sb.append("\nmRecentAvailableNetworks: ").append(mRecentAvailableNetworks.size());
    405         for (Map.Entry<String, AvailableNetworkFailureCount> entry
    406                 : mRecentAvailableNetworks.entrySet()) {
    407             sb.append("\n ").append(entry.getKey()).append(": ").append(entry.getValue())
    408                 .append(", Age: ").append(entry.getValue().age);
    409         }
    410         sb.append("\nmSsidFailureCount:");
    411         for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry :
    412                 mSsidFailureCount.entrySet()) {
    413             final AvailableNetworkFailureCount failureCount = entry.getValue().first;
    414             final Integer apCount = entry.getValue().second;
    415             sb.append("\n").append(entry.getKey()).append(": ").append(apCount).append(",")
    416                     .append(failureCount.toString());
    417         }
    418         return sb.toString();
    419     }
    420 
    421     /**
    422      * @param bssid bssid to check the failures for
    423      * @return true if any failure count is over FAILURE_THRESHOLD
    424      */
    425     public boolean isOverFailureThreshold(String bssid) {
    426         if ((getFailureCount(bssid, FAILURE_CODE_ASSOCIATION) >= FAILURE_THRESHOLD)
    427                 || (getFailureCount(bssid, FAILURE_CODE_AUTHENTICATION) >= FAILURE_THRESHOLD)
    428                 || (getFailureCount(bssid, FAILURE_CODE_DHCP) >= FAILURE_THRESHOLD)) {
    429             return true;
    430         }
    431         return false;
    432     }
    433 
    434     /**
    435      * Get the failure count for a specific bssid. This actually checks the ssid attached to the
    436      * BSSID and returns the SSID count
    437      * @param reason failure reason to get count for
    438      */
    439     public int getFailureCount(String bssid, int reason) {
    440         AvailableNetworkFailureCount availableNetworkFailureCount =
    441                 mRecentAvailableNetworks.get(bssid);
    442         if (availableNetworkFailureCount == null) {
    443             return 0;
    444         }
    445         String ssid = availableNetworkFailureCount.ssid;
    446         Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
    447         if (ssidFails == null) {
    448             Log.d(TAG, "getFailureCount: Could not find SSID count for " + ssid);
    449             return 0;
    450         }
    451         final AvailableNetworkFailureCount failCount = ssidFails.first;
    452         switch (reason) {
    453             case FAILURE_CODE_ASSOCIATION:
    454                 return failCount.associationRejection;
    455             case FAILURE_CODE_AUTHENTICATION:
    456                 return failCount.authenticationFailure;
    457             case FAILURE_CODE_DHCP:
    458                 return failCount.dhcpFailure;
    459             default:
    460                 return 0;
    461         }
    462     }
    463 
    464     protected void enableVerboseLogging(int verbose) {
    465         if (verbose > 0) {
    466             mVerboseLoggingEnabled = true;
    467         } else {
    468             mVerboseLoggingEnabled = false;
    469         }
    470     }
    471 
    472     /**
    473      * This class holds the failure counts for an 'available network' (one of the potential
    474      * candidates for connection, as determined by framework).
    475      */
    476     public static class AvailableNetworkFailureCount {
    477         /**
    478          * WifiConfiguration associated with this network. Can be null for Ephemeral networks
    479          */
    480         public WifiConfiguration config;
    481         /**
    482         * SSID of the network (from ScanDetail)
    483         */
    484         public String ssid = "";
    485         /**
    486          * Number of times network has failed due to Association Rejection
    487          */
    488         public int associationRejection = 0;
    489         /**
    490          * Number of times network has failed due to Authentication Failure or SSID_TEMP_DISABLED
    491          */
    492         public int authenticationFailure = 0;
    493         /**
    494          * Number of times network has failed due to DHCP failure
    495          */
    496         public int dhcpFailure = 0;
    497         /**
    498          * Number of scanResults since this network was last seen
    499          */
    500         public int age = 0;
    501 
    502         AvailableNetworkFailureCount(WifiConfiguration configParam) {
    503             this.config = configParam;
    504         }
    505 
    506         /**
    507          * @param reason failure reason to increment count for
    508          */
    509         public void incrementFailureCount(int reason) {
    510             switch (reason) {
    511                 case FAILURE_CODE_ASSOCIATION:
    512                     associationRejection++;
    513                     break;
    514                 case FAILURE_CODE_AUTHENTICATION:
    515                     authenticationFailure++;
    516                     break;
    517                 case FAILURE_CODE_DHCP:
    518                     dhcpFailure++;
    519                     break;
    520                 default: //do nothing
    521             }
    522         }
    523 
    524         /**
    525          * Set all failure counts for this network to 0
    526          */
    527         void resetCounts() {
    528             associationRejection = 0;
    529             authenticationFailure = 0;
    530             dhcpFailure = 0;
    531         }
    532 
    533         public String toString() {
    534             return  ssid + " HasEverConnected: " + ((config != null)
    535                     ? config.getNetworkSelectionStatus().getHasEverConnected() : "null_config")
    536                     + ", Failures: {"
    537                     + "Assoc: " + associationRejection
    538                     + ", Auth: " + authenticationFailure
    539                     + ", Dhcp: " + dhcpFailure
    540                     + "}";
    541         }
    542     }
    543 }
    544