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