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