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