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 
WifiLastResortWatchdog(WifiMetrics wifiMetrics)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      */
updateAvailableNetworks( List<Pair<ScanDetail, WifiConfiguration>> availableNetworks)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      */
noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason)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      */
connectedStateTransition(boolean isEntering)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      */
updateFailureCountForNetwork(String ssid, String bssid, int reason)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      */
incrementSsidFailureCount(String ssid, int reason)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      */
incrementBssidFailureCount(String ssid, String bssid, int reason)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      */
checkTriggerCondition()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      */
restartWifiStack()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      */
incrementWifiMetricsTriggerCounts()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      */
clearAllFailureCounts()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      */
getRecentAvailableNetworks()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      */
setWatchdogTriggerEnabled(boolean enable)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      */
toString()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      */
isOverFailureThreshold(String bssid)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      */
getFailureCount(String bssid, int reason)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 
AvailableNetworkFailureCount(WifiConfiguration configParam)498         AvailableNetworkFailureCount(WifiConfiguration configParam) {
499             this.config = configParam;
500         }
501 
502         /**
503          * @param reason failure reason to increment count for
504          */
incrementFailureCount(int reason)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          */
resetCounts()523         void resetCounts() {
524             associationRejection = 0;
525             authenticationFailure = 0;
526             dhcpFailure = 0;
527         }
528 
toString()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