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