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