WifiLastResortWatchdog.java revision 11ad3437e833ead2c7c235f173824db16ee4ea02
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
17package com.android.server.wifi;
18
19import android.net.wifi.ScanResult;
20import android.net.wifi.WifiConfiguration;
21import android.util.Log;
22import android.util.Pair;
23
24import java.util.HashMap;
25import java.util.Iterator;
26import java.util.List;
27import 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 */
35public 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