• 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 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 
WifiLastResortWatchdog(WifiMetrics wifiMetrics)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      */
updateAvailableNetworks( List<Pair<ScanDetail, WifiConfiguration>> availableNetworks)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      */
noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason)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      */
connectedStateTransition(boolean isEntering)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      */
updateFailureCountForNetwork(String ssid, String bssid, int reason)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      */
incrementSsidFailureCount(String ssid, int reason)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      */
incrementBssidFailureCount(String ssid, String bssid, int reason)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      */
checkTriggerCondition()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      */
restartWifiStack()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      */
incrementWifiMetricsTriggerCounts()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      */
clearAllFailureCounts()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      */
getRecentAvailableNetworks()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      */
setWatchdogTriggerEnabled(boolean enable)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      */
toString()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      */
isOverFailureThreshold(String bssid)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      */
getFailureCount(String bssid, int reason)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 
AvailableNetworkFailureCount(WifiConfiguration configParam)508         AvailableNetworkFailureCount(WifiConfiguration configParam) {
509             this.config = configParam;
510         }
511 
512         /**
513          * @param reason failure reason to increment count for
514          */
incrementFailureCount(int reason)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          */
resetCounts()533         void resetCounts() {
534             associationRejection = 0;
535             authenticationFailure = 0;
536             dhcpFailure = 0;
537         }
538 
toString()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      */
setWifiController(WifiController wifiController)557     public void setWifiController(WifiController wifiController) {
558         mWifiController = wifiController;
559     }
560 }
561