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
213        if (!mWatchdogAllowedToTrigger) {
214            // WiFi has connected after a Watchdog trigger, without any new networks becoming
215            // available, log a Watchdog success in wifi metrics
216            mWifiMetrics.incrementNumLastResortWatchdogSuccesses();
217        }
218        if (isEntering) {
219            // We connected to something! Reset failure counts for everything
220            clearAllFailureCounts();
221            // If the watchdog trigger was disabled (it triggered), connecting means we did
222            // something right, re-enable it so it can fire again.
223            setWatchdogTriggerEnabled(true);
224        }
225    }
226
227    /**
228     * Increments the failure reason count for the given network, in 'mSsidFailureCount'
229     * Failures are counted per SSID, either; by using the ssid string when the bssid is "any"
230     * or by looking up the ssid attached to a specific bssid
231     * An unused set of counts is also kept which is bssid specific, in 'mRecentAvailableNetworks'
232     * @param ssid of the network that has failed connection
233     * @param bssid of the network that has failed connection, can be "any"
234     * @param reason Message id from WifiStateMachine for this failure
235     */
236    private void updateFailureCountForNetwork(String ssid, String bssid, int reason) {
237        if (VDBG) {
238            Log.v(TAG, "updateFailureCountForNetwork: [" + ssid + ", " + bssid + ", "
239                    + reason + "]");
240        }
241        if (BSSID_ANY.equals(bssid)) {
242            incrementSsidFailureCount(ssid, reason);
243        } else {
244            // Bssid count is actually unused except for logging purposes
245            // SSID count is incremented within the BSSID counting method
246            incrementBssidFailureCount(ssid, bssid, reason);
247        }
248    }
249
250    /**
251     * Update the per-SSID failure count
252     * @param ssid the ssid to increment failure count for
253     * @param reason the failure type to increment count for
254     */
255    private void incrementSsidFailureCount(String ssid, int reason) {
256        Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
257        if (ssidFails == null) {
258            if (DBG) {
259                Log.v(TAG, "updateFailureCountForNetwork: No networks for ssid = " + ssid);
260            }
261            return;
262        }
263        AvailableNetworkFailureCount failureCount = ssidFails.first;
264        failureCount.incrementFailureCount(reason);
265    }
266
267    /**
268     * Update the per-BSSID failure count
269     * @param bssid the bssid to increment failure count for
270     * @param reason the failure type to increment count for
271     */
272    private void incrementBssidFailureCount(String ssid, String bssid, int reason) {
273        AvailableNetworkFailureCount availableNetworkFailureCount =
274                mRecentAvailableNetworks.get(bssid);
275        if (availableNetworkFailureCount == null) {
276            if (DBG) {
277                Log.d(TAG, "updateFailureCountForNetwork: Unable to find Network [" + ssid
278                        + ", " + bssid + "]");
279            }
280            return;
281        }
282        if (!availableNetworkFailureCount.ssid.equals(ssid)) {
283            if (DBG) {
284                Log.d(TAG, "updateFailureCountForNetwork: Failed connection attempt has"
285                        + " wrong ssid. Failed [" + ssid + ", " + bssid + "], buffered ["
286                        + availableNetworkFailureCount.ssid + ", " + bssid + "]");
287            }
288            return;
289        }
290        if (availableNetworkFailureCount.config == null) {
291            if (VDBG) {
292                Log.v(TAG, "updateFailureCountForNetwork: network has no config ["
293                        + ssid + ", " + bssid + "]");
294            }
295        }
296        availableNetworkFailureCount.incrementFailureCount(reason);
297        incrementSsidFailureCount(ssid, reason);
298    }
299
300    /**
301     * Check trigger condition: For all available networks, have we met a failure threshold for each
302     * of them, and have previously connected to at-least one of the available networks
303     * @return is the trigger condition true
304     */
305    private boolean checkTriggerCondition() {
306        if (VDBG) Log.v(TAG, "checkTriggerCondition.");
307        // Don't check Watchdog trigger if wifi is in a connected state
308        // (This should not occur, but we want to protect against any race conditions)
309        if (mWifiIsConnected) return false;
310        // Don't check Watchdog trigger if trigger is not enabled
311        if (!mWatchdogAllowedToTrigger) return false;
312
313        boolean atleastOneNetworkHasEverConnected = false;
314        for (Map.Entry<String, AvailableNetworkFailureCount> entry
315                : mRecentAvailableNetworks.entrySet()) {
316            if (entry.getValue().config != null
317                    && entry.getValue().config.getNetworkSelectionStatus().getHasEverConnected()) {
318                atleastOneNetworkHasEverConnected = true;
319            }
320            if (!isOverFailureThreshold(entry.getKey())) {
321                // This available network is not over failure threshold, meaning we still have a
322                // network to try connecting to
323                return false;
324            }
325        }
326        // We have met the failure count for every available network & there is at-least one network
327        // we have previously connected to present.
328        if (VDBG) {
329            Log.v(TAG, "checkTriggerCondition: return = " + atleastOneNetworkHasEverConnected);
330        }
331        return atleastOneNetworkHasEverConnected;
332    }
333
334    /**
335     * Trigger a restart of the wifi stack.
336     */
337    private void restartWifiStack() {
338        if (VDBG) Log.v(TAG, "restartWifiStack.");
339
340        // First verify that we can send the trigger message.
341        if (mWifiController == null) {
342            Log.e(TAG, "WifiLastResortWatchdog unable to trigger: WifiController is null");
343            return;
344        }
345
346        if (DBG) Log.d(TAG, toString());
347
348        mWifiController.sendMessage(WifiController.CMD_RESTART_WIFI);
349        Log.i(TAG, "Triggered WiFi stack restart.");
350    }
351
352    /**
353     * Update WifiMetrics with various Watchdog stats (trigger counts, failed network counts)
354     */
355    private void incrementWifiMetricsTriggerCounts() {
356        if (VDBG) Log.v(TAG, "incrementWifiMetricsTriggerCounts.");
357        mWifiMetrics.incrementNumLastResortWatchdogTriggers();
358        mWifiMetrics.addCountToNumLastResortWatchdogAvailableNetworksTotal(
359                mSsidFailureCount.size());
360        // Number of networks over each failure type threshold, present at trigger time
361        int badAuth = 0;
362        int badAssoc = 0;
363        int badDhcp = 0;
364        for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry
365                : mSsidFailureCount.entrySet()) {
366            badAuth += (entry.getValue().first.authenticationFailure >= FAILURE_THRESHOLD) ? 1 : 0;
367            badAssoc += (entry.getValue().first.associationRejection >= FAILURE_THRESHOLD) ? 1 : 0;
368            badDhcp += (entry.getValue().first.dhcpFailure >= FAILURE_THRESHOLD) ? 1 : 0;
369        }
370        if (badAuth > 0) {
371            mWifiMetrics.addCountToNumLastResortWatchdogBadAuthenticationNetworksTotal(badAuth);
372            mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAuthentication();
373        }
374        if (badAssoc > 0) {
375            mWifiMetrics.addCountToNumLastResortWatchdogBadAssociationNetworksTotal(badAssoc);
376            mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAssociation();
377        }
378        if (badDhcp > 0) {
379            mWifiMetrics.addCountToNumLastResortWatchdogBadDhcpNetworksTotal(badDhcp);
380            mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadDhcp();
381        }
382    }
383
384    /**
385     * Clear failure counts for each network in recentAvailableNetworks
386     */
387    private void clearAllFailureCounts() {
388        if (VDBG) Log.v(TAG, "clearAllFailureCounts.");
389        for (Map.Entry<String, AvailableNetworkFailureCount> entry
390                : mRecentAvailableNetworks.entrySet()) {
391            final AvailableNetworkFailureCount failureCount = entry.getValue();
392            entry.getValue().resetCounts();
393        }
394        for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry
395                : mSsidFailureCount.entrySet()) {
396            final AvailableNetworkFailureCount failureCount = entry.getValue().first;
397            failureCount.resetCounts();
398        }
399    }
400    /**
401     * Gets the buffer of recently available networks
402     */
403    Map<String, AvailableNetworkFailureCount> getRecentAvailableNetworks() {
404        return mRecentAvailableNetworks;
405    }
406
407    /**
408     * Activates or deactivates the Watchdog trigger. Counting and network buffering still occurs
409     * @param enable true to enable the Watchdog trigger, false to disable it
410     */
411    private void setWatchdogTriggerEnabled(boolean enable) {
412        if (VDBG) Log.v(TAG, "setWatchdogTriggerEnabled: enable = " + enable);
413        mWatchdogAllowedToTrigger = enable;
414    }
415
416    /**
417     * Prints all networks & counts within mRecentAvailableNetworks to string
418     */
419    public String toString() {
420        StringBuilder sb = new StringBuilder();
421        sb.append("mWatchdogAllowedToTrigger: ").append(mWatchdogAllowedToTrigger);
422        sb.append("\nmWifiIsConnected: ").append(mWifiIsConnected);
423        sb.append("\nmRecentAvailableNetworks: ").append(mRecentAvailableNetworks.size());
424        for (Map.Entry<String, AvailableNetworkFailureCount> entry
425                : mRecentAvailableNetworks.entrySet()) {
426            sb.append("\n ").append(entry.getKey()).append(": ").append(entry.getValue());
427        }
428        sb.append("\nmSsidFailureCount:");
429        for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry :
430                mSsidFailureCount.entrySet()) {
431            final AvailableNetworkFailureCount failureCount = entry.getValue().first;
432            final Integer apCount = entry.getValue().second;
433            sb.append("\n").append(entry.getKey()).append(": ").append(apCount).append(", ")
434                    .append(failureCount.toString());
435        }
436        return sb.toString();
437    }
438
439    /**
440     * @param bssid bssid to check the failures for
441     * @return true if any failure count is over FAILURE_THRESHOLD
442     */
443    public boolean isOverFailureThreshold(String bssid) {
444        if ((getFailureCount(bssid, FAILURE_CODE_ASSOCIATION) >= FAILURE_THRESHOLD)
445                || (getFailureCount(bssid, FAILURE_CODE_AUTHENTICATION) >= FAILURE_THRESHOLD)
446                || (getFailureCount(bssid, FAILURE_CODE_DHCP) >= FAILURE_THRESHOLD)) {
447            return true;
448        }
449        return false;
450    }
451
452    /**
453     * Get the failure count for a specific bssid. This actually checks the ssid attached to the
454     * BSSID and returns the SSID count
455     * @param reason failure reason to get count for
456     */
457    public int getFailureCount(String bssid, int reason) {
458        AvailableNetworkFailureCount availableNetworkFailureCount =
459                mRecentAvailableNetworks.get(bssid);
460        if (availableNetworkFailureCount == null) {
461            return 0;
462        }
463        String ssid = availableNetworkFailureCount.ssid;
464        Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
465        if (ssidFails == null) {
466            if (DBG) {
467                Log.d(TAG, "getFailureCount: Could not find SSID count for " + ssid);
468            }
469            return 0;
470        }
471        final AvailableNetworkFailureCount failCount = ssidFails.first;
472        switch (reason) {
473            case FAILURE_CODE_ASSOCIATION:
474                return failCount.associationRejection;
475            case FAILURE_CODE_AUTHENTICATION:
476                return failCount.authenticationFailure;
477            case FAILURE_CODE_DHCP:
478                return failCount.dhcpFailure;
479            default:
480                return 0;
481        }
482    }
483
484    /**
485     * This class holds the failure counts for an 'available network' (one of the potential
486     * candidates for connection, as determined by framework).
487     */
488    public static class AvailableNetworkFailureCount {
489        /**
490         * WifiConfiguration associated with this network. Can be null for Ephemeral networks
491         */
492        public WifiConfiguration config;
493        /**
494        * SSID of the network (from ScanDetail)
495        */
496        public String ssid = "";
497        /**
498         * Number of times network has failed due to Association Rejection
499         */
500        public int associationRejection = 0;
501        /**
502         * Number of times network has failed due to Authentication Failure or SSID_TEMP_DISABLED
503         */
504        public int authenticationFailure = 0;
505        /**
506         * Number of times network has failed due to DHCP failure
507         */
508        public int dhcpFailure = 0;
509        /**
510         * Number of scanResults since this network was last seen
511         */
512        public int age = 0;
513
514        AvailableNetworkFailureCount(WifiConfiguration configParam) {
515            this.config = configParam;
516        }
517
518        /**
519         * @param reason failure reason to increment count for
520         */
521        public void incrementFailureCount(int reason) {
522            switch (reason) {
523                case FAILURE_CODE_ASSOCIATION:
524                    associationRejection++;
525                    break;
526                case FAILURE_CODE_AUTHENTICATION:
527                    authenticationFailure++;
528                    break;
529                case FAILURE_CODE_DHCP:
530                    dhcpFailure++;
531                    break;
532                default: //do nothing
533            }
534        }
535
536        /**
537         * Set all failure counts for this network to 0
538         */
539        void resetCounts() {
540            associationRejection = 0;
541            authenticationFailure = 0;
542            dhcpFailure = 0;
543        }
544
545        public String toString() {
546            return  ssid + ", HasEverConnected: " + ((config != null)
547                    ? config.getNetworkSelectionStatus().getHasEverConnected() : "null_config")
548                    + ", Failures: {"
549                    + "Assoc: " + associationRejection
550                    + ", Auth: " + authenticationFailure
551                    + ", Dhcp: " + dhcpFailure
552                    + "}"
553                    + ", Age: " + age;
554        }
555    }
556
557    /**
558     * Method used to set the WifiController for the this watchdog.
559     *
560     * The WifiController is used to send the restart wifi command to carry out the wifi restart.
561     * @param wifiController WifiController instance that will be sent the CMD_RESTART_WIFI message.
562     */
563    public void setWifiController(WifiController wifiController) {
564        mWifiController = wifiController;
565    }
566}
567