WifiLastResortWatchdog.java revision 8fe3e3497daf08b71ffc8c33cb7b139df6667448
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
76    /**
77     * Refreshes recentAvailableNetworks with the latest available networks
78     * Adds new networks, removes old ones that have timed out. Should be called after Wifi
79     * framework decides what networks it is potentially connecting to.
80     * @param availableNetworks ScanDetail & Config list of potential connection
81     * candidates
82     */
83    public void updateAvailableNetworks(
84            List<Pair<ScanDetail, WifiConfiguration>> availableNetworks) {
85        if (VDBG) Log.v(TAG, "updateAvailableNetworks: size = " + availableNetworks.size());
86        // Add new networks to mRecentAvailableNetworks
87        if (availableNetworks != null) {
88            for (Pair<ScanDetail, WifiConfiguration> pair : availableNetworks) {
89                final ScanDetail scanDetail = pair.first;
90                final WifiConfiguration config = pair.second;
91                ScanResult scanResult = scanDetail.getScanResult();
92                if (scanResult == null) continue;
93                String bssid = scanResult.BSSID;
94                String ssid = "\"" + scanDetail.getSSID() + "\"";
95                if (VDBG) Log.v(TAG, " " + bssid + ": " + scanDetail.getSSID());
96                // Cache the scanResult & WifiConfig
97                AvailableNetworkFailureCount availableNetworkFailureCount =
98                        mRecentAvailableNetworks.get(bssid);
99                if (availableNetworkFailureCount == null) {
100                    // New network is available
101                    availableNetworkFailureCount = new AvailableNetworkFailureCount(config);
102                    availableNetworkFailureCount.ssid = ssid;
103
104                    // Count AP for this SSID
105                    Pair<AvailableNetworkFailureCount, Integer> ssidFailsAndApCount =
106                            mSsidFailureCount.get(ssid);
107                    if (ssidFailsAndApCount == null) {
108                        // This is a new SSID, create new FailureCount for it and set AP count to 1
109                        ssidFailsAndApCount = Pair.create(new AvailableNetworkFailureCount(config),
110                                1);
111                    } else {
112                        final Integer numberOfAps = ssidFailsAndApCount.second;
113                        // This is not a new SSID, increment the AP count for it
114                        ssidFailsAndApCount = Pair.create(ssidFailsAndApCount.first,
115                                numberOfAps + 1);
116                    }
117                    mSsidFailureCount.put(ssid, ssidFailsAndApCount);
118                }
119                // refresh config
120                availableNetworkFailureCount.config = config;
121                // If we saw a network, set its Age to -1 here, aging iteration will set it to 0
122                availableNetworkFailureCount.age = -1;
123                mRecentAvailableNetworks.put(bssid, availableNetworkFailureCount);
124            }
125        }
126
127        // Iterate through available networks updating timeout counts & removing networks.
128        Iterator<Map.Entry<String, AvailableNetworkFailureCount>> it =
129                mRecentAvailableNetworks.entrySet().iterator();
130        while (it.hasNext()) {
131            Map.Entry<String, AvailableNetworkFailureCount> entry = it.next();
132            if (entry.getValue().age < MAX_BSSID_AGE - 1) {
133                entry.getValue().age++;
134            } else {
135                // Decrement this SSID : AP count
136                String ssid = entry.getValue().ssid;
137                Pair<AvailableNetworkFailureCount, Integer> ssidFails =
138                            mSsidFailureCount.get(ssid);
139                if (ssidFails != null) {
140                    Integer apCount = ssidFails.second - 1;
141                    if (apCount > 0) {
142                        ssidFails = Pair.create(ssidFails.first, apCount);
143                        mSsidFailureCount.put(ssid, ssidFails);
144                    } else {
145                        mSsidFailureCount.remove(ssid);
146                    }
147                } else {
148                    if (DBG) {
149                        Log.d(TAG, "updateAvailableNetworks: SSID to AP count mismatch for "
150                                + ssid);
151                    }
152                }
153                it.remove();
154            }
155        }
156        if (VDBG) Log.v(TAG, toString());
157    }
158
159    /**
160     * Increments the failure reason count for the given bssid. Performs a check to see if we have
161     * exceeded a failure threshold for all available networks, and executes the last resort restart
162     * @param bssid of the network that has failed connection, can be "any"
163     * @param reason Message id from WifiStateMachine for this failure
164     * @return true if watchdog triggers, returned for test visibility
165     */
166    public boolean noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason) {
167        if (VDBG) {
168            Log.v(TAG, "noteConnectionFailureAndTriggerIfNeeded: [" + ssid + ", " + bssid + ", "
169                    + reason + "]");
170        }
171        // Update failure count for the failing network
172        updateFailureCountForNetwork(ssid, bssid, reason);
173        return false;
174    }
175
176    /**
177     * Increments the failure reason count for the given network, in 'mSsidFailureCount'
178     * Failures are counted per SSID, either; by using the ssid string when the bssid is "any"
179     * or by looking up the ssid attached to a specific bssid
180     * An unused set of counts is also kept which is bssid specific, in 'mRecentAvailableNetworks'
181     * @param ssid of the network that has failed connection
182     * @param bssid of the network that has failed connection, can be "any"
183     * @param reason Message id from WifiStateMachine for this failure
184     */
185    private void updateFailureCountForNetwork(String ssid, String bssid, int reason) {
186        if (VDBG) {
187            Log.v(TAG, "updateFailureCountForNetwork: [" + ssid + ", " + bssid + ", "
188                    + reason + "]");
189        }
190        if (BSSID_ANY.equals(bssid)) {
191            incrementSsidFailureCount(ssid, reason);
192        } else {
193            // Bssid count is actually unused except for logging purposes
194            // SSID count is incremented within the BSSID counting method
195            incrementBssidFailureCount(ssid, bssid, reason);
196        }
197    }
198
199    /**
200     * Update the per-SSID failure count
201     * @param ssid the ssid to increment failure count for
202     * @param reason the failure type to increment count for
203     */
204    private void incrementSsidFailureCount(String ssid, int reason) {
205        Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
206        if (ssidFails == null) {
207            if (DBG) {
208                Log.v(TAG, "updateFailureCountForNetwork: No networks for ssid = " + ssid);
209            }
210            return;
211        }
212        AvailableNetworkFailureCount failureCount = ssidFails.first;
213        failureCount.incrementFailureCount(reason);
214    }
215
216    /**
217     * Update the per-BSSID failure count
218     * @param bssid the bssid to increment failure count for
219     * @param reason the failure type to increment count for
220     */
221    private void incrementBssidFailureCount(String ssid, String bssid, int reason) {
222        AvailableNetworkFailureCount availableNetworkFailureCount =
223                mRecentAvailableNetworks.get(bssid);
224        if (availableNetworkFailureCount == null) {
225            if (DBG) {
226                Log.d(TAG, "updateFailureCountForNetwork: Unable to find Network [" + ssid
227                        + ", " + bssid + "]");
228            }
229            return;
230        }
231        if (!availableNetworkFailureCount.ssid.equals(ssid)) {
232            if (DBG) {
233                Log.d(TAG, "updateFailureCountForNetwork: Failed connection attempt has"
234                        + " wrong ssid. Failed [" + ssid + ", " + bssid + "], buffered ["
235                        + availableNetworkFailureCount.ssid + ", " + bssid + "]");
236            }
237            return;
238        }
239        if (availableNetworkFailureCount.config == null) {
240            if (VDBG) {
241                Log.v(TAG, "updateFailureCountForNetwork: network has no config ["
242                        + ssid + ", " + bssid + "]");
243            }
244        }
245        availableNetworkFailureCount.incrementFailureCount(reason);
246        incrementSsidFailureCount(ssid, reason);
247    }
248
249    /**
250     * Clear failure counts for each network in recentAvailableNetworks
251     */
252    private void clearAllFailureCounts() {
253        if (VDBG) Log.v(TAG, "clearAllFailureCounts.");
254        for (Map.Entry<String, AvailableNetworkFailureCount> entry
255                : mRecentAvailableNetworks.entrySet()) {
256            final AvailableNetworkFailureCount failureCount = entry.getValue();
257            entry.getValue().resetCounts();
258        }
259        for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry
260                : mSsidFailureCount.entrySet()) {
261            final AvailableNetworkFailureCount failureCount = entry.getValue().first;
262            failureCount.resetCounts();
263        }
264    }
265    /**
266     * Gets the buffer of recently available networks
267     */
268    Map<String, AvailableNetworkFailureCount> getRecentAvailableNetworks() {
269        return mRecentAvailableNetworks;
270    }
271
272    /**
273     * Prints all networks & counts within mRecentAvailableNetworks to string
274     */
275    public String toString() {
276        StringBuilder sb = new StringBuilder();
277        sb.append("WifiLastResortWatchdog: " + mRecentAvailableNetworks.size() + " networks...");
278        for (Map.Entry<String, AvailableNetworkFailureCount> entry
279                : mRecentAvailableNetworks.entrySet()) {
280            sb.append("\n " + entry.getKey() + ": " + entry.getValue());
281        }
282        sb.append("\nmSsidFailureCount:");
283        for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry :
284                mSsidFailureCount.entrySet()) {
285            final AvailableNetworkFailureCount failureCount = entry.getValue().first;
286            final Integer apCount = entry.getValue().second;
287            sb.append("\n" + entry.getKey() + ": " + apCount + ", "
288                    + failureCount.toString());
289        }
290        return sb.toString();
291    }
292
293    /**
294     * @param bssid bssid to check the failures for
295     * @return true if any failure count is over FAILURE_THRESHOLD
296     */
297    public boolean isOverFailureThreshold(String bssid) {
298        if ((getFailureCount(bssid, FAILURE_CODE_ASSOCIATION) >= FAILURE_THRESHOLD)
299                || (getFailureCount(bssid, FAILURE_CODE_AUTHENTICATION) >= FAILURE_THRESHOLD)
300                || (getFailureCount(bssid, FAILURE_CODE_DHCP) >= FAILURE_THRESHOLD)) {
301            return true;
302        }
303        return false;
304    }
305
306    /**
307     * Get the failure count for a specific bssid. This actually checks the ssid attached to the
308     * BSSID and returns the SSID count
309     * @param reason failure reason to get count for
310     */
311    public int getFailureCount(String bssid, int reason) {
312        AvailableNetworkFailureCount availableNetworkFailureCount =
313                mRecentAvailableNetworks.get(bssid);
314        if (availableNetworkFailureCount == null) {
315            return 0;
316        }
317        String ssid = availableNetworkFailureCount.ssid;
318        Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
319        if (ssidFails == null) {
320            if (DBG) {
321                Log.d(TAG, "getFailureCount: Could not find SSID count for " + ssid);
322            }
323            return 0;
324        }
325        final AvailableNetworkFailureCount failCount = ssidFails.first;
326        switch (reason) {
327            case FAILURE_CODE_ASSOCIATION:
328                return failCount.associationRejection;
329            case FAILURE_CODE_AUTHENTICATION:
330                return failCount.authenticationFailure;
331            case FAILURE_CODE_DHCP:
332                return failCount.dhcpFailure;
333            default:
334                return 0;
335        }
336    }
337
338    /**
339     * This class holds the failure counts for an 'available network' (one of the potential
340     * candidates for connection, as determined by framework).
341     */
342    public static class AvailableNetworkFailureCount {
343        /**
344         * WifiConfiguration associated with this network. Can be null for Ephemeral networks
345         */
346        public WifiConfiguration config;
347        /**
348        * SSID of the network (from ScanDetail)
349        */
350        public String ssid = "";
351        /**
352         * Number of times network has failed due to Association Rejection
353         */
354        public int associationRejection = 0;
355        /**
356         * Number of times network has failed due to Authentication Failure or SSID_TEMP_DISABLED
357         */
358        public int authenticationFailure = 0;
359        /**
360         * Number of times network has failed due to DHCP failure
361         */
362        public int dhcpFailure = 0;
363        /**
364         * Number of scanResults since this network was last seen
365         */
366        public int age = 0;
367
368        AvailableNetworkFailureCount(WifiConfiguration config) {
369            config = config;
370        }
371
372        /**
373         * @param reason failure reason to increment count for
374         */
375        public void incrementFailureCount(int reason) {
376            switch (reason) {
377                case FAILURE_CODE_ASSOCIATION:
378                    associationRejection++;
379                    break;
380                case FAILURE_CODE_AUTHENTICATION:
381                    authenticationFailure++;
382                    break;
383                case FAILURE_CODE_DHCP:
384                    dhcpFailure++;
385                    break;
386                default: //do nothing
387            }
388        }
389
390        /**
391         * Set all failure counts for this network to 0
392         */
393        void resetCounts() {
394            associationRejection = 0;
395            authenticationFailure = 0;
396            dhcpFailure = 0;
397        }
398
399        public String toString() {
400            return  ssid + ", HasEverConnected: " + ((config != null)
401                    ? config.getNetworkSelectionStatus().getHasEverConnected() : false)
402                    + ", Failures: {"
403                    + "Assoc: " + associationRejection
404                    + ", Auth: " + authenticationFailure
405                    + ", Dhcp: " + dhcpFailure
406                    + "}"
407                    + ", Age: " + age;
408        }
409    }
410}
411