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