• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
WifiLastResortWatchdog(SelfRecovery selfRecovery, WifiMetrics wifiMetrics)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      */
updateAvailableNetworks( List<Pair<ScanDetail, WifiConfiguration>> availableNetworks)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      */
noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason)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      */
connectedStateTransition(boolean isEntering)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      */
updateFailureCountForNetwork(String ssid, String bssid, int reason)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      */
incrementSsidFailureCount(String ssid, int reason)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      */
incrementBssidFailureCount(String ssid, String bssid, int reason)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      */
checkTriggerCondition()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      */
incrementWifiMetricsTriggerCounts()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      */
clearAllFailureCounts()368     public 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      */
getRecentAvailableNetworks()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      */
setWatchdogTriggerEnabled(boolean enable)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      */
toString()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      */
isOverFailureThreshold(String bssid)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      */
getFailureCount(String bssid, int reason)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 
enableVerboseLogging(int verbose)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 
AvailableNetworkFailureCount(WifiConfiguration configParam)502         AvailableNetworkFailureCount(WifiConfiguration configParam) {
503             this.config = configParam;
504         }
505 
506         /**
507          * @param reason failure reason to increment count for
508          */
incrementFailureCount(int reason)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          */
resetCounts()527         void resetCounts() {
528             associationRejection = 0;
529             authenticationFailure = 0;
530             dhcpFailure = 0;
531         }
532 
toString()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