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 
17 package com.android.server.wifi;
18 
19 import android.content.Context;
20 import android.net.wifi.ScanResult;
21 import android.net.wifi.WifiConfiguration;
22 import android.os.Handler;
23 import android.os.Looper;
24 import android.os.Message;
25 import android.text.TextUtils;
26 import android.util.LocalLog;
27 import android.util.Log;
28 import android.util.Pair;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.wifi.resources.R;
32 
33 import java.io.FileDescriptor;
34 import java.io.PrintWriter;
35 import java.util.HashMap;
36 import java.util.HashSet;
37 import java.util.Iterator;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Set;
41 
42 /**
43  * This Class is a Work-In-Progress, intended behavior is as follows:
44  * Essentially this class automates a user toggling 'Airplane Mode' when WiFi "won't work".
45  * IF each available saved network has failed connecting more times than the FAILURE_THRESHOLD
46  * THEN Watchdog will restart Supplicant, wifi driver and return ClientModeImpl to InitialState.
47  */
48 public class WifiLastResortWatchdog {
49     private static final String TAG = "WifiLastResortWatchdog";
50     private boolean mVerboseLoggingEnabled = false;
51     /**
52      * Association Failure code
53      */
54     public static final int FAILURE_CODE_ASSOCIATION = 1;
55     /**
56      * Authentication Failure code
57      */
58     public static final int FAILURE_CODE_AUTHENTICATION = 2;
59     /**
60      * Dhcp Failure code
61      */
62     public static final int FAILURE_CODE_DHCP = 3;
63     /**
64      * Maximum number of scan results received since we last saw a BSSID.
65      * If it is not seen before this limit is reached, the network is culled
66      */
67     public static final int MAX_BSSID_AGE = 10;
68     /**
69      * BSSID used to increment failure counts against ALL bssids associated with a particular SSID
70      */
71     public static final String BSSID_ANY = "any";
72     /**
73      * Failure count that each available networks must meet to possibly trigger the Watchdog
74      */
75     public static final int FAILURE_THRESHOLD = 7;
76     public static final String BUGREPORT_TITLE = "Wifi watchdog triggered";
77     public static final double PROB_TAKE_BUGREPORT_DEFAULT = 1;
78 
79     // Number of milliseconds to wait before re-enable Watchdog triger
80     @VisibleForTesting
81     public static final long LAST_TRIGGER_TIMEOUT_MILLIS = 2 * 3600 * 1000; // 2 hours
82 
83 
84     /**
85      * Cached WifiConfigurations of available networks seen within MAX_BSSID_AGE scan results
86      * Key:BSSID, Value:Counters of failure types
87      */
88     private Map<String, AvailableNetworkFailureCount> mRecentAvailableNetworks = new HashMap<>();
89 
90     /**
91      * Map of SSID to <FailureCount, AP count>, used to count failures & number of access points
92      * belonging to an SSID.
93      */
94     private Map<String, Pair<AvailableNetworkFailureCount, Integer>> mSsidFailureCount =
95             new HashMap<>();
96 
97     /* List of failure BSSID */
98     private Set<String> mBssidFailureList = new HashSet<>();
99 
100     // Tracks: if ClientModeImpl is in ConnectedState
101     private boolean mWifiIsConnected = false;
102     // Is Watchdog allowed to trigger now? Set to false after triggering. Set to true after
103     // successfully connecting or a new network (SSID) becomes available to connect to.
104     private boolean mWatchdogAllowedToTrigger = true;
105     private long mTimeLastTrigger = 0;
106     private String mSsidLastTrigger = null;
107 
108     private WifiInjector mWifiInjector;
109     private WifiMetrics mWifiMetrics;
110     private ClientModeImpl mClientModeImpl;
111     private Looper mClientModeImplLooper;
112     private double mBugReportProbability = PROB_TAKE_BUGREPORT_DEFAULT;
113     private Clock mClock;
114     private Context mContext;
115     private DeviceConfigFacade mDeviceConfigFacade;
116     // If any connection failure happened after watchdog triggering restart then assume watchdog
117     // did not fix the problem
118     private boolean mWatchdogFixedWifi = true;
119     private long mLastStartConnectTime = 0;
120     private final Handler mHandler;
121     private final WifiThreadRunner mWifiThreadRunner;
122 
123     private Boolean mWatchdogFeatureEnabled = null;
124 
125     /**
126      * Local log used for debugging any WifiLastResortWatchdog issues.
127      */
128     private final LocalLog mLocalLog = new LocalLog(100);
129 
WifiLastResortWatchdog(WifiInjector wifiInjector, Context context, Clock clock, WifiMetrics wifiMetrics, ClientModeImpl clientModeImpl, Looper clientModeImplLooper, DeviceConfigFacade deviceConfigFacade, WifiThreadRunner wifiThreadRunner)130     WifiLastResortWatchdog(WifiInjector wifiInjector, Context context, Clock clock,
131             WifiMetrics wifiMetrics, ClientModeImpl clientModeImpl, Looper clientModeImplLooper,
132             DeviceConfigFacade deviceConfigFacade, WifiThreadRunner wifiThreadRunner) {
133         mWifiInjector = wifiInjector;
134         mClock = clock;
135         mWifiMetrics = wifiMetrics;
136         mClientModeImpl = clientModeImpl;
137         mClientModeImplLooper = clientModeImplLooper;
138         mContext = context;
139         mDeviceConfigFacade = deviceConfigFacade;
140         mWifiThreadRunner = wifiThreadRunner;
141         mHandler = new Handler(clientModeImplLooper) {
142             public void handleMessage(Message msg) {
143                 processMessage(msg);
144             }
145         };
146     }
147 
148     /**
149      * Returns handler for L2 events from supplicant.
150      * @return Handler
151      */
getHandler()152     public Handler getHandler() {
153         return mHandler;
154     }
155 
156     /**
157      * Refreshes when the last CMD_START_CONNECT is triggered.
158      */
noteStartConnectTime()159     public void noteStartConnectTime() {
160         mHandler.post(() -> {
161             mLastStartConnectTime = mClock.getElapsedSinceBootMillis();
162         });
163     }
164 
processMessage(Message msg)165     private void processMessage(Message msg) {
166         switch (msg.what) {
167             case WifiMonitor.NETWORK_CONNECTION_EVENT:
168                 // Trigger bugreport for successful connections that take abnormally long
169                 if (mDeviceConfigFacade.isAbnormalConnectionBugreportEnabled()
170                         && mLastStartConnectTime > 0) {
171                     long durationMs = mClock.getElapsedSinceBootMillis() - mLastStartConnectTime;
172                     long abnormalConnectionDurationMs =
173                             mDeviceConfigFacade.getAbnormalConnectionDurationMs();
174                     if (durationMs > abnormalConnectionDurationMs) {
175                         final String bugTitle = "Wi-Fi Bugreport: Abnormal connection time";
176                         final String bugDetail = "Expected connection to take less than "
177                                 + abnormalConnectionDurationMs + " milliseconds. "
178                                 + "Actually took " + durationMs + " milliseconds.";
179                         logv("Triggering bug report for abnormal connection time.");
180                         mWifiThreadRunner.post(() ->
181                                 mClientModeImpl.takeBugReport(bugTitle, bugDetail));
182                     }
183                 }
184                 // Should reset last connection time after each connection regardless if bugreport
185                 // is enabled or not.
186                 mLastStartConnectTime = 0;
187                 break;
188             default:
189                 return;
190         }
191     }
192 
193     /**
194      * Refreshes recentAvailableNetworks with the latest available networks
195      * Adds new networks, removes old ones that have timed out. Should be called after Wifi
196      * framework decides what networks it is potentially connecting to.
197      * @param availableNetworks ScanDetail & Config list of potential connection
198      * candidates
199      */
updateAvailableNetworks( List<Pair<ScanDetail, WifiConfiguration>> availableNetworks)200     public void updateAvailableNetworks(
201             List<Pair<ScanDetail, WifiConfiguration>> availableNetworks) {
202         // Add new networks to mRecentAvailableNetworks
203         if (availableNetworks != null) {
204             if (mVerboseLoggingEnabled) {
205                 Log.v(TAG, "updateAvailableNetworks: size = " + availableNetworks.size());
206             }
207             for (Pair<ScanDetail, WifiConfiguration> pair : availableNetworks) {
208                 final ScanDetail scanDetail = pair.first;
209                 final WifiConfiguration config = pair.second;
210                 ScanResult scanResult = scanDetail.getScanResult();
211                 if (scanResult == null) continue;
212                 String bssid = scanResult.BSSID;
213                 String ssid = "\"" + scanDetail.getSSID() + "\"";
214                 if (mVerboseLoggingEnabled) {
215                     Log.v(TAG, " " + bssid + ": " + scanDetail.getSSID());
216                 }
217                 // Cache the scanResult & WifiConfig
218                 AvailableNetworkFailureCount availableNetworkFailureCount =
219                         mRecentAvailableNetworks.get(bssid);
220                 if (availableNetworkFailureCount == null) {
221                     // New network is available
222                     availableNetworkFailureCount = new AvailableNetworkFailureCount(config);
223                     availableNetworkFailureCount.ssid = ssid;
224 
225                     // Count AP for this SSID
226                     Pair<AvailableNetworkFailureCount, Integer> ssidFailsAndApCount =
227                             mSsidFailureCount.get(ssid);
228                     if (ssidFailsAndApCount == null) {
229                         // This is a new SSID, create new FailureCount for it and set AP count to 1
230                         ssidFailsAndApCount = Pair.create(new AvailableNetworkFailureCount(config),
231                                 1);
232                         // Do not re-enable Watchdog in LAST_TRIGGER_TIMEOUT_MILLIS
233                         // after last time Watchdog be triggered
234                         if (!mWatchdogAllowedToTrigger && (mTimeLastTrigger == 0
235                                 || (mClock.getElapsedSinceBootMillis() - mTimeLastTrigger)
236                                     >= LAST_TRIGGER_TIMEOUT_MILLIS)) {
237                             localLog("updateAvailableNetworks: setWatchdogTriggerEnabled to true");
238                             setWatchdogTriggerEnabled(true);
239                         }
240                     } else {
241                         final Integer numberOfAps = ssidFailsAndApCount.second;
242                         // This is not a new SSID, increment the AP count for it
243                         ssidFailsAndApCount = Pair.create(ssidFailsAndApCount.first,
244                                 numberOfAps + 1);
245                     }
246                     mSsidFailureCount.put(ssid, ssidFailsAndApCount);
247                 }
248                 // refresh config if it is not null
249                 if (config != null) {
250                     availableNetworkFailureCount.config = config;
251                 }
252                 // If we saw a network, set its Age to -1 here, aging iteration will set it to 0
253                 availableNetworkFailureCount.age = -1;
254                 mRecentAvailableNetworks.put(bssid, availableNetworkFailureCount);
255             }
256         }
257 
258         // Iterate through available networks updating timeout counts & removing networks.
259         Iterator<Map.Entry<String, AvailableNetworkFailureCount>> it =
260                 mRecentAvailableNetworks.entrySet().iterator();
261         while (it.hasNext()) {
262             Map.Entry<String, AvailableNetworkFailureCount> entry = it.next();
263             if (entry.getValue().age < MAX_BSSID_AGE - 1) {
264                 entry.getValue().age++;
265             } else {
266                 // Decrement this SSID : AP count
267                 String ssid = entry.getValue().ssid;
268                 Pair<AvailableNetworkFailureCount, Integer> ssidFails =
269                             mSsidFailureCount.get(ssid);
270                 if (ssidFails != null) {
271                     Integer apCount = ssidFails.second - 1;
272                     if (apCount > 0) {
273                         ssidFails = Pair.create(ssidFails.first, apCount);
274                         mSsidFailureCount.put(ssid, ssidFails);
275                     } else {
276                         mSsidFailureCount.remove(ssid);
277                     }
278                 } else {
279                     Log.d(TAG, "updateAvailableNetworks: SSID to AP count mismatch for " + ssid);
280                 }
281                 it.remove();
282             }
283         }
284         if (mVerboseLoggingEnabled) Log.v(TAG, toString());
285     }
286 
287     /**
288      * Increments the failure reason count for the given bssid. Performs a check to see if we have
289      * exceeded a failure threshold for all available networks, and executes the last resort restart
290      * @param bssid of the network that has failed connection, can be "any"
291      * @param reason Message id from ClientModeImpl for this failure
292      * @return true if watchdog triggers, returned for test visibility
293      */
noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason)294     public boolean noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason) {
295         if (mVerboseLoggingEnabled) {
296             Log.v(TAG, "noteConnectionFailureAndTriggerIfNeeded: [" + ssid + ", " + bssid + ", "
297                     + reason + "]");
298         }
299 
300         // Update failure count for the failing network
301         updateFailureCountForNetwork(ssid, bssid, reason);
302 
303         // If watchdog is not allowed to trigger it means a wifi restart is already triggered
304         if (!mWatchdogAllowedToTrigger) {
305             mWifiMetrics.incrementWatchdogTotalConnectionFailureCountAfterTrigger();
306             mWatchdogFixedWifi = false;
307         }
308         // Have we met conditions to trigger the Watchdog Wifi restart?
309         boolean isRestartNeeded = checkTriggerCondition();
310         if (mVerboseLoggingEnabled) {
311             Log.v(TAG, "isRestartNeeded = " + isRestartNeeded);
312         }
313         if (isRestartNeeded) {
314             if (getWifiWatchdogFeature()) {
315                 // Stop the watchdog from triggering until re-enabled
316                 localLog("Trigger recovery: setWatchdogTriggerEnabled to false");
317                 setWatchdogTriggerEnabled(false);
318                 mWatchdogFixedWifi = true;
319                 loge("Watchdog triggering recovery");
320                 mSsidLastTrigger = ssid;
321                 mTimeLastTrigger = mClock.getElapsedSinceBootMillis();
322                 localLog(toString());
323                 mWifiInjector.getSelfRecovery().trigger(SelfRecovery.REASON_LAST_RESORT_WATCHDOG);
324                 incrementWifiMetricsTriggerCounts();
325             } else {
326                 // auto bugreport if issue happens
327                 loge("bugreport notification");
328                 setWatchdogTriggerEnabled(false);
329                 takeBugReportWithCurrentProbability("Wifi Watchdog bite");
330             }
331         }
332         return isRestartNeeded;
333     }
334 
335     /**
336      * Handles transitions entering and exiting ClientModeImpl ConnectedState
337      * Used to track wifistate, and perform watchdog count resetting
338      * @param isEntering true if called from ConnectedState.enter(), false for exit()
339      */
connectedStateTransition(boolean isEntering)340     public void connectedStateTransition(boolean isEntering) {
341         logv("connectedStateTransition: isEntering = " + isEntering);
342 
343         mWifiIsConnected = isEntering;
344         if (!isEntering) {
345             return;
346         }
347         if (!mWatchdogAllowedToTrigger && mWatchdogFixedWifi
348                 && getWifiWatchdogFeature()
349                 && checkIfAtleastOneNetworkHasEverConnected()
350                 && checkIfConnectedBackToSameSsid()
351                 && checkIfConnectedBssidHasEverFailed()) {
352             takeBugReportWithCurrentProbability("Wifi fixed after restart");
353             // WiFi has connected after a Watchdog trigger, without any new networks becoming
354             // available, log a Watchdog success in wifi metrics
355             mWifiMetrics.incrementNumLastResortWatchdogSuccesses();
356             long durationMs = mClock.getElapsedSinceBootMillis() - mTimeLastTrigger;
357             mWifiMetrics.setWatchdogSuccessTimeDurationMs(durationMs);
358         }
359         // If the watchdog trigger was disabled (it triggered), connecting means we did
360         // something right, re-enable it so it can fire again.
361         localLog("connectedStateTransition: setWatchdogTriggerEnabled to true");
362         setWatchdogTriggerEnabled(true);
363     }
364 
365     /**
366      * Helper function to check if device connected to BSSID
367      * which is in BSSID failure list after watchdog trigger.
368      */
checkIfConnectedBssidHasEverFailed()369     private boolean checkIfConnectedBssidHasEverFailed() {
370         return mBssidFailureList.contains(mClientModeImpl.getWifiInfo().getBSSID());
371     }
372 
373     /**
374      * Helper function to check if device connect back to same
375      * SSID after watchdog trigger
376      */
checkIfConnectedBackToSameSsid()377     private boolean checkIfConnectedBackToSameSsid() {
378         if (TextUtils.equals(mSsidLastTrigger, mClientModeImpl.getWifiInfo().getSSID())) {
379             return true;
380         }
381         localLog("checkIfConnectedBackToSameSsid: different SSID be connected");
382         return false;
383     }
384 
385     /**
386      * Triggers a wifi specific bugreport with a based on the current trigger probability.
387      * @param bugDetail description of the bug
388      */
takeBugReportWithCurrentProbability(String bugDetail)389     private void takeBugReportWithCurrentProbability(String bugDetail) {
390         if (mBugReportProbability <= Math.random()) {
391             return;
392         }
393         (new Handler(mClientModeImplLooper)).post(() -> {
394             mClientModeImpl.takeBugReport(BUGREPORT_TITLE, bugDetail);
395         });
396     }
397 
398     /**
399      * Increments the failure reason count for the given network, in 'mSsidFailureCount'
400      * Failures are counted per SSID, either; by using the ssid string when the bssid is "any"
401      * or by looking up the ssid attached to a specific bssid
402      * An unused set of counts is also kept which is bssid specific, in 'mRecentAvailableNetworks'
403      * @param ssid of the network that has failed connection
404      * @param bssid of the network that has failed connection, can be "any"
405      * @param reason Message id from ClientModeImpl for this failure
406      */
updateFailureCountForNetwork(String ssid, String bssid, int reason)407     private void updateFailureCountForNetwork(String ssid, String bssid, int reason) {
408         logv("updateFailureCountForNetwork: [" + ssid + ", " + bssid + ", "
409                 + reason + "]");
410         if (BSSID_ANY.equals(bssid)) {
411             incrementSsidFailureCount(ssid, reason);
412         } else {
413             // Bssid count is actually unused except for logging purposes
414             // SSID count is incremented within the BSSID counting method
415             incrementBssidFailureCount(ssid, bssid, reason);
416             mBssidFailureList.add(bssid);
417         }
418     }
419 
420     /**
421      * Update the per-SSID failure count
422      * @param ssid the ssid to increment failure count for
423      * @param reason the failure type to increment count for
424      */
incrementSsidFailureCount(String ssid, int reason)425     private void incrementSsidFailureCount(String ssid, int reason) {
426         Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
427         if (ssidFails == null) {
428             Log.d(TAG, "updateFailureCountForNetwork: No networks for ssid = " + ssid);
429             return;
430         }
431         AvailableNetworkFailureCount failureCount = ssidFails.first;
432         failureCount.incrementFailureCount(reason);
433     }
434 
435     /**
436      * Update the per-BSSID failure count
437      * @param bssid the bssid to increment failure count for
438      * @param reason the failure type to increment count for
439      */
incrementBssidFailureCount(String ssid, String bssid, int reason)440     private void incrementBssidFailureCount(String ssid, String bssid, int reason) {
441         AvailableNetworkFailureCount availableNetworkFailureCount =
442                 mRecentAvailableNetworks.get(bssid);
443         if (availableNetworkFailureCount == null) {
444             Log.d(TAG, "updateFailureCountForNetwork: Unable to find Network [" + ssid
445                     + ", " + bssid + "]");
446             return;
447         }
448         if (!availableNetworkFailureCount.ssid.equals(ssid)) {
449             Log.d(TAG, "updateFailureCountForNetwork: Failed connection attempt has"
450                     + " wrong ssid. Failed [" + ssid + ", " + bssid + "], buffered ["
451                     + availableNetworkFailureCount.ssid + ", " + bssid + "]");
452             return;
453         }
454         if (availableNetworkFailureCount.config == null) {
455             if (mVerboseLoggingEnabled) {
456                 Log.v(TAG, "updateFailureCountForNetwork: network has no config ["
457                         + ssid + ", " + bssid + "]");
458             }
459         }
460         availableNetworkFailureCount.incrementFailureCount(reason);
461         incrementSsidFailureCount(ssid, reason);
462     }
463 
464     /**
465      * Helper function to check if we should ignore BSSID update.
466      * @param bssid BSSID of the access point
467      * @return true if we should ignore BSSID update
468      */
shouldIgnoreBssidUpdate(String bssid)469     public boolean shouldIgnoreBssidUpdate(String bssid) {
470         return mWatchdogAllowedToTrigger
471                 && isBssidOnlyApOfSsid(bssid)
472                 && isSingleSsidRecorded()
473                 && checkIfAtleastOneNetworkHasEverConnected();
474     }
475 
476     /**
477      * Helper function to check if we should ignore SSID update.
478      * @return true if should ignore SSID update
479      */
shouldIgnoreSsidUpdate()480     public boolean shouldIgnoreSsidUpdate() {
481         return mWatchdogAllowedToTrigger
482                 && isSingleSsidRecorded()
483                 && checkIfAtleastOneNetworkHasEverConnected();
484     }
485 
486     /**
487      * Check the specified BSSID is the only BSSID for its corresponding SSID.
488      * @param bssid BSSID of the access point
489      * @return true if only BSSID for its corresponding SSID be observed
490      */
isBssidOnlyApOfSsid(String bssid)491     private boolean isBssidOnlyApOfSsid(String bssid) {
492         AvailableNetworkFailureCount availableNetworkFailureCount =
493                 mRecentAvailableNetworks.get(bssid);
494         if (availableNetworkFailureCount == null) {
495             return false;
496         }
497         String ssid = availableNetworkFailureCount.ssid;
498         Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
499         if (ssidFails == null) {
500             Log.d(TAG, "isOnlyBssidAvailable: Could not find SSID count for " + ssid);
501             return false;
502         }
503         if (ssidFails.second != 1) {
504             return false;
505         }
506         return true;
507     }
508 
509     /**
510      * Check there is only single SSID be observed.
511      * @return true if only single SSID be observed.
512      */
isSingleSsidRecorded()513     private boolean isSingleSsidRecorded() {
514         return (mSsidFailureCount.size() == 1);
515     }
516 
517     /**
518      * Check trigger condition: For all available networks, have we met a failure threshold for each
519      * of them, and have previously connected to at-least one of the available networks
520      * @return is the trigger condition true
521      */
checkTriggerCondition()522     private boolean checkTriggerCondition() {
523         if (mVerboseLoggingEnabled) Log.v(TAG, "checkTriggerCondition.");
524         // Don't check Watchdog trigger if wifi is in a connected state
525         // (This should not occur, but we want to protect against any race conditions)
526         if (mWifiIsConnected) return false;
527         // Don't check Watchdog trigger if trigger is not enabled
528         if (!mWatchdogAllowedToTrigger) return false;
529 
530         for (Map.Entry<String, AvailableNetworkFailureCount> entry
531                 : mRecentAvailableNetworks.entrySet()) {
532             if (!isOverFailureThreshold(entry.getKey())) {
533                 // This available network is not over failure threshold, meaning we still have a
534                 // network to try connecting to
535                 return false;
536             }
537         }
538         // We have met the failure count for every available network.
539         // Trigger restart if there exists at-least one network that we have previously connected.
540         boolean atleastOneNetworkHasEverConnected = checkIfAtleastOneNetworkHasEverConnected();
541         logv("checkTriggerCondition: return = " + atleastOneNetworkHasEverConnected);
542         return checkIfAtleastOneNetworkHasEverConnected();
543     }
544 
checkIfAtleastOneNetworkHasEverConnected()545     private boolean checkIfAtleastOneNetworkHasEverConnected() {
546         for (Map.Entry<String, AvailableNetworkFailureCount> entry
547                 : mRecentAvailableNetworks.entrySet()) {
548             if (entry.getValue().config != null
549                     && entry.getValue().config.getNetworkSelectionStatus().hasEverConnected()) {
550                 return true;
551             }
552         }
553         return false;
554     }
555 
556     /**
557      * Update WifiMetrics with various Watchdog stats (trigger counts, failed network counts)
558      */
incrementWifiMetricsTriggerCounts()559     private void incrementWifiMetricsTriggerCounts() {
560         if (mVerboseLoggingEnabled) Log.v(TAG, "incrementWifiMetricsTriggerCounts.");
561         mWifiMetrics.incrementNumLastResortWatchdogTriggers();
562         mWifiMetrics.addCountToNumLastResortWatchdogAvailableNetworksTotal(
563                 mSsidFailureCount.size());
564         // Number of networks over each failure type threshold, present at trigger time
565         int badAuth = 0;
566         int badAssoc = 0;
567         int badDhcp = 0;
568         int badSum = 0;
569         for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry
570                 : mSsidFailureCount.entrySet()) {
571             badSum = entry.getValue().first.associationRejection
572                     + entry.getValue().first.authenticationFailure
573                     + entry.getValue().first.dhcpFailure;
574             // count as contributor if over half of badSum.
575             if (badSum >= FAILURE_THRESHOLD) {
576                 badAssoc += (entry.getValue().first.associationRejection >= badSum / 2) ? 1 : 0;
577                 badAuth += (entry.getValue().first.authenticationFailure >= badSum / 2) ? 1 : 0;
578                 badDhcp += (entry.getValue().first.dhcpFailure >= badSum / 2) ? 1 : 0;
579             }
580         }
581         if (badAuth > 0) {
582             mWifiMetrics.addCountToNumLastResortWatchdogBadAuthenticationNetworksTotal(badAuth);
583             mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAuthentication();
584         }
585         if (badAssoc > 0) {
586             mWifiMetrics.addCountToNumLastResortWatchdogBadAssociationNetworksTotal(badAssoc);
587             mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAssociation();
588         }
589         if (badDhcp > 0) {
590             mWifiMetrics.addCountToNumLastResortWatchdogBadDhcpNetworksTotal(badDhcp);
591             mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadDhcp();
592         }
593     }
594 
595     /**
596      * Clear all failure counts
597      */
clearAllFailureCounts()598     public void clearAllFailureCounts() {
599         if (mVerboseLoggingEnabled) Log.v(TAG, "clearAllFailureCounts.");
600         for (Map.Entry<String, AvailableNetworkFailureCount> entry
601                 : mRecentAvailableNetworks.entrySet()) {
602             final AvailableNetworkFailureCount failureCount = entry.getValue();
603             failureCount.resetCounts();
604         }
605         for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry
606                 : mSsidFailureCount.entrySet()) {
607             final AvailableNetworkFailureCount failureCount = entry.getValue().first;
608             failureCount.resetCounts();
609         }
610         mBssidFailureList.clear();
611     }
612     /**
613      * Gets the buffer of recently available networks
614      */
getRecentAvailableNetworks()615     Map<String, AvailableNetworkFailureCount> getRecentAvailableNetworks() {
616         return mRecentAvailableNetworks;
617     }
618 
619     /**
620      * Activates or deactivates the Watchdog trigger. Counting and network buffering still occurs
621      * @param enable true to enable the Watchdog trigger, false to disable it
622      */
setWatchdogTriggerEnabled(boolean enable)623     private void setWatchdogTriggerEnabled(boolean enable) {
624         if (mVerboseLoggingEnabled) Log.v(TAG, "setWatchdogTriggerEnabled: enable = " + enable);
625         // Reset failure counts before actives watchdog
626         if (enable) {
627             clearAllFailureCounts();
628         }
629         mWatchdogAllowedToTrigger = enable;
630     }
631 
632     /**
633      * Prints all networks & counts within mRecentAvailableNetworks to string
634      */
toString()635     public String toString() {
636         StringBuilder sb = new StringBuilder();
637         sb.append("mWatchdogFeatureEnabled: ").append(getWifiWatchdogFeature());
638         sb.append("\nmWatchdogAllowedToTrigger: ").append(mWatchdogAllowedToTrigger);
639         sb.append("\nmWifiIsConnected: ").append(mWifiIsConnected);
640         sb.append("\nmRecentAvailableNetworks: ").append(mRecentAvailableNetworks.size());
641         for (Map.Entry<String, AvailableNetworkFailureCount> entry
642                 : mRecentAvailableNetworks.entrySet()) {
643             sb.append("\n ").append(entry.getKey()).append(": ").append(entry.getValue())
644                 .append(", Age: ").append(entry.getValue().age);
645         }
646         sb.append("\nmSsidFailureCount:");
647         for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry :
648                 mSsidFailureCount.entrySet()) {
649             final AvailableNetworkFailureCount failureCount = entry.getValue().first;
650             final Integer apCount = entry.getValue().second;
651             sb.append("\n").append(entry.getKey()).append(": ").append(apCount).append(",")
652                     .append(failureCount.toString());
653         }
654         return sb.toString();
655     }
656 
657     /**
658      * @param bssid bssid to check the failures for
659      * @return true if sum of failure count is over FAILURE_THRESHOLD
660      */
isOverFailureThreshold(String bssid)661     public boolean isOverFailureThreshold(String bssid) {
662         return (getFailureCount(bssid, FAILURE_CODE_ASSOCIATION)
663                 + getFailureCount(bssid, FAILURE_CODE_AUTHENTICATION)
664                 + getFailureCount(bssid, FAILURE_CODE_DHCP)) >= FAILURE_THRESHOLD;
665     }
666 
667     /**
668      * Get the failure count for a specific bssid. This actually checks the ssid attached to the
669      * BSSID and returns the SSID count
670      * @param reason failure reason to get count for
671      */
getFailureCount(String bssid, int reason)672     public int getFailureCount(String bssid, int reason) {
673         AvailableNetworkFailureCount availableNetworkFailureCount =
674                 mRecentAvailableNetworks.get(bssid);
675         if (availableNetworkFailureCount == null) {
676             return 0;
677         }
678         String ssid = availableNetworkFailureCount.ssid;
679         Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
680         if (ssidFails == null) {
681             Log.d(TAG, "getFailureCount: Could not find SSID count for " + ssid);
682             return 0;
683         }
684         final AvailableNetworkFailureCount failCount = ssidFails.first;
685         switch (reason) {
686             case FAILURE_CODE_ASSOCIATION:
687                 return failCount.associationRejection;
688             case FAILURE_CODE_AUTHENTICATION:
689                 return failCount.authenticationFailure;
690             case FAILURE_CODE_DHCP:
691                 return failCount.dhcpFailure;
692             default:
693                 return 0;
694         }
695     }
696 
697     /**
698      * Sets whether wifi watchdog should trigger recovery
699      */
setWifiWatchdogFeature(boolean enable)700     public void setWifiWatchdogFeature(boolean enable) {
701         logv("setWifiWatchdogFeature: " + enable);
702         mWatchdogFeatureEnabled = enable;
703         // for debugging purpose, reset mWatchdogAllowedToTrigger as well
704         setWatchdogTriggerEnabled(true);
705     }
706 
707     /**
708      * Returns whether wifi watchdog should trigger recovery.
709      */
getWifiWatchdogFeature()710     public boolean getWifiWatchdogFeature() {
711         if (mWatchdogFeatureEnabled == null) {
712             mWatchdogFeatureEnabled = mContext.getResources().getBoolean(
713                     R.bool.config_wifi_watchdog_enabled);
714         }
715         return mWatchdogFeatureEnabled;
716     }
717 
enableVerboseLogging(int verbose)718     protected void enableVerboseLogging(int verbose) {
719         if (verbose > 0) {
720             mVerboseLoggingEnabled = true;
721         } else {
722             mVerboseLoggingEnabled = false;
723         }
724     }
725 
726     @VisibleForTesting
setBugReportProbability(double newProbability)727     protected void setBugReportProbability(double newProbability) {
728         mBugReportProbability = newProbability;
729     }
730 
731     /**
732      * This class holds the failure counts for an 'available network' (one of the potential
733      * candidates for connection, as determined by framework).
734      */
735     public static class AvailableNetworkFailureCount {
736         /**
737          * WifiConfiguration associated with this network. Can be null for Ephemeral networks
738          */
739         public WifiConfiguration config;
740         /**
741         * SSID of the network (from ScanDetail)
742         */
743         public String ssid = "";
744         /**
745          * Number of times network has failed due to Association Rejection
746          */
747         public int associationRejection = 0;
748         /**
749          * Number of times network has failed due to Authentication Failure or SSID_TEMP_DISABLED
750          */
751         public int authenticationFailure = 0;
752         /**
753          * Number of times network has failed due to DHCP failure
754          */
755         public int dhcpFailure = 0;
756         /**
757          * Number of scanResults since this network was last seen
758          */
759         public int age = 0;
760 
AvailableNetworkFailureCount(WifiConfiguration configParam)761         AvailableNetworkFailureCount(WifiConfiguration configParam) {
762             this.config = configParam;
763         }
764 
765         /**
766          * @param reason failure reason to increment count for
767          */
incrementFailureCount(int reason)768         public void incrementFailureCount(int reason) {
769             switch (reason) {
770                 case FAILURE_CODE_ASSOCIATION:
771                     associationRejection++;
772                     break;
773                 case FAILURE_CODE_AUTHENTICATION:
774                     authenticationFailure++;
775                     break;
776                 case FAILURE_CODE_DHCP:
777                     dhcpFailure++;
778                     break;
779                 default: //do nothing
780             }
781         }
782 
783         /**
784          * Set all failure counts for this network to 0
785          */
resetCounts()786         void resetCounts() {
787             associationRejection = 0;
788             authenticationFailure = 0;
789             dhcpFailure = 0;
790         }
791 
toString()792         public String toString() {
793             return  ssid + " HasEverConnected: " + ((config != null)
794                     ? config.getNetworkSelectionStatus().hasEverConnected() : "null_config")
795                     + ", Failures: {"
796                     + "Assoc: " + associationRejection
797                     + ", Auth: " + authenticationFailure
798                     + ", Dhcp: " + dhcpFailure
799                     + "}";
800         }
801     }
802 
803     /**
804      * Helper function for logging into local log buffer.
805      */
localLog(String s)806     private void localLog(String s) {
807         mLocalLog.log(s);
808     }
809 
logv(String s)810     private void logv(String s) {
811         mLocalLog.log(s);
812         if (mVerboseLoggingEnabled) {
813             Log.v(TAG, s);
814         }
815     }
816 
loge(String s)817     private void loge(String s) {
818         mLocalLog.log(s);
819         Log.e(TAG, s);
820     }
821 
822     /**
823      * Dump the local log buffer and other internal state of WifiLastResortWatchdog.
824      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)825     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
826         pw.println("Dump of WifiLastResortWatchdog");
827         pw.println("WifiLastResortWatchdog - Log Begin ----");
828         mLocalLog.dump(fd, pw, args);
829         pw.println("WifiLastResortWatchdog - Log End ----");
830     }
831 }
832