• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.syncRequestConnectionInfo();
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                     }
222                 }
223                 // Should reset last connection time after each connection regardless if bugreport
224                 // is enabled or not.
225                 mNetworkIdToLastStartConnectTimeMillisSinceBoot.remove(networkId);
226                 break;
227             }
228             default:
229                 return;
230         }
231     }
232 
233     /**
234      * Refreshes recentAvailableNetworks with the latest available networks
235      * Adds new networks, removes old ones that have timed out. Should be called after Wifi
236      * framework decides what networks it is potentially connecting to.
237      * @param availableNetworks ScanDetail & Config list of potential connection
238      * candidates
239      */
updateAvailableNetworks( List<Pair<ScanDetail, WifiConfiguration>> availableNetworks)240     public void updateAvailableNetworks(
241             List<Pair<ScanDetail, WifiConfiguration>> availableNetworks) {
242         // Add new networks to mRecentAvailableNetworks
243         if (availableNetworks != null) {
244             if (mVerboseLoggingEnabled) {
245                 Log.v(TAG, "updateAvailableNetworks: size = " + availableNetworks.size());
246             }
247             for (Pair<ScanDetail, WifiConfiguration> pair : availableNetworks) {
248                 final ScanDetail scanDetail = pair.first;
249                 final WifiConfiguration config = pair.second;
250                 ScanResult scanResult = scanDetail.getScanResult();
251                 if (scanResult == null) continue;
252                 String bssid = scanResult.BSSID;
253                 String ssid = "\"" + scanDetail.getSSID() + "\"";
254                 if (mVerboseLoggingEnabled) {
255                     Log.v(TAG, " " + bssid + ": " + scanDetail.getSSID());
256                 }
257                 // Cache the scanResult & WifiConfig
258                 AvailableNetworkFailureCount availableNetworkFailureCount =
259                         mRecentAvailableNetworks.get(bssid);
260                 if (availableNetworkFailureCount == null) {
261                     // New network is available
262                     availableNetworkFailureCount = new AvailableNetworkFailureCount(config);
263                     availableNetworkFailureCount.ssid = ssid;
264 
265                     // Count AP for this SSID
266                     Pair<AvailableNetworkFailureCount, Integer> ssidFailsAndApCount =
267                             mSsidFailureCount.get(ssid);
268                     if (ssidFailsAndApCount == null) {
269                         // This is a new SSID, create new FailureCount for it and set AP count to 1
270                         ssidFailsAndApCount = Pair.create(new AvailableNetworkFailureCount(config),
271                                 1);
272                         // Do not re-enable Watchdog in LAST_TRIGGER_TIMEOUT_MILLIS
273                         // after last time Watchdog be triggered
274                         if (!mWatchdogAllowedToTrigger && (mTimeLastTrigger == 0
275                                 || (mClock.getElapsedSinceBootMillis() - mTimeLastTrigger)
276                                     >= LAST_TRIGGER_TIMEOUT_MILLIS)) {
277                             localLog("updateAvailableNetworks: setWatchdogTriggerEnabled to true");
278                             setWatchdogTriggerEnabled(true);
279                         }
280                     } else {
281                         final Integer numberOfAps = ssidFailsAndApCount.second;
282                         // This is not a new SSID, increment the AP count for it
283                         ssidFailsAndApCount = Pair.create(ssidFailsAndApCount.first,
284                                 numberOfAps + 1);
285                     }
286                     mSsidFailureCount.put(ssid, ssidFailsAndApCount);
287                 }
288                 // refresh config if it is not null
289                 if (config != null) {
290                     availableNetworkFailureCount.config = config;
291                 }
292                 // If we saw a network, set its Age to -1 here, aging iteration will set it to 0
293                 availableNetworkFailureCount.age = -1;
294                 mRecentAvailableNetworks.put(bssid, availableNetworkFailureCount);
295             }
296         }
297 
298         // Iterate through available networks updating timeout counts & removing networks.
299         Iterator<Map.Entry<String, AvailableNetworkFailureCount>> it =
300                 mRecentAvailableNetworks.entrySet().iterator();
301         while (it.hasNext()) {
302             Map.Entry<String, AvailableNetworkFailureCount> entry = it.next();
303             if (entry.getValue().age < MAX_BSSID_AGE - 1) {
304                 entry.getValue().age++;
305             } else {
306                 // Decrement this SSID : AP count
307                 String ssid = entry.getValue().ssid;
308                 Pair<AvailableNetworkFailureCount, Integer> ssidFails =
309                             mSsidFailureCount.get(ssid);
310                 if (ssidFails != null) {
311                     Integer apCount = ssidFails.second - 1;
312                     if (apCount > 0) {
313                         ssidFails = Pair.create(ssidFails.first, apCount);
314                         mSsidFailureCount.put(ssid, ssidFails);
315                     } else {
316                         mSsidFailureCount.remove(ssid);
317                     }
318                 } else {
319                     Log.d(TAG, "updateAvailableNetworks: SSID to AP count mismatch for " + ssid);
320                 }
321                 it.remove();
322             }
323         }
324         if (mVerboseLoggingEnabled) Log.v(TAG, toString());
325     }
326 
327     /**
328      * Increments the failure reason count for the given bssid. Performs a check to see if we have
329      * exceeded a failure threshold for all available networks, and executes the last resort restart
330      * @param bssid of the network that has failed connection, can be "any"
331      * @param reason Message id from ClientModeImpl for this failure
332      * @param isConnected whether the ClientModeImpl is currently connected
333      * @return true if watchdog triggers, returned for test visibility
334      */
noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason, boolean isConnected)335     public boolean noteConnectionFailureAndTriggerIfNeeded(String ssid, String bssid, int reason,
336             boolean isConnected) {
337         if (mVerboseLoggingEnabled) {
338             Log.v(TAG, "noteConnectionFailureAndTriggerIfNeeded: [" + ssid + ", " + bssid + ", "
339                     + reason + "]");
340         }
341 
342         // Update failure count for the failing network
343         updateFailureCountForNetwork(ssid, bssid, reason);
344 
345         // If watchdog is not allowed to trigger it means a wifi restart is already triggered
346         if (!mWatchdogAllowedToTrigger) {
347             mWifiMetrics.incrementWatchdogTotalConnectionFailureCountAfterTrigger();
348             mWatchdogFixedWifi = false;
349         }
350         // Have we met conditions to trigger the Watchdog Wifi restart?
351         boolean isRestartNeeded = checkTriggerCondition(isConnected);
352         if (mVerboseLoggingEnabled) {
353             Log.v(TAG, "isRestartNeeded = " + isRestartNeeded);
354         }
355         if (isRestartNeeded) {
356             if (getWifiWatchdogFeature()) {
357                 // Stop the watchdog from triggering until re-enabled
358                 localLog("Trigger recovery: setWatchdogTriggerEnabled to false");
359                 setWatchdogTriggerEnabled(false);
360                 mWatchdogFixedWifi = true;
361                 loge("Watchdog triggering recovery");
362                 mSsidLastTrigger = ssid;
363                 mTimeLastTrigger = mClock.getElapsedSinceBootMillis();
364                 localLog(toString());
365                 mWifiInjector.getSelfRecovery().trigger(SelfRecovery.REASON_LAST_RESORT_WATCHDOG);
366                 incrementWifiMetricsTriggerCounts();
367             } else {
368                 // auto bugreport if issue happens
369                 loge("bugreport notification");
370                 setWatchdogTriggerEnabled(false);
371                 takeBugReportWithCurrentProbability("Wifi Watchdog bite");
372             }
373         }
374         return isRestartNeeded;
375     }
376 
377     /**
378      * Handles transitions entering and exiting ClientModeImpl ConnectedState
379      * Used to track wifistate, and perform watchdog count resetting
380      * @param isEntering true if called from ConnectedState.enter(), false for exit()
381      */
connectedStateTransition(boolean isEntering)382     public void connectedStateTransition(boolean isEntering) {
383         logv("connectedStateTransition: isEntering = " + isEntering);
384 
385         if (!isEntering) {
386             return;
387         }
388         WifiInfo wifiInfo = getPrimaryWifiInfo();
389         if (!mWatchdogAllowedToTrigger && mWatchdogFixedWifi
390                 && getWifiWatchdogFeature()
391                 && checkIfAtleastOneNetworkHasEverConnected()
392                 && checkIfConnectedBackToSameSsid(wifiInfo)
393                 && checkIfConnectedBssidHasEverFailed(wifiInfo)) {
394             takeBugReportWithCurrentProbability("Wifi fixed after restart");
395             // WiFi has connected after a Watchdog trigger, without any new networks becoming
396             // available, log a Watchdog success in wifi metrics
397             mWifiMetrics.incrementNumLastResortWatchdogSuccesses();
398             long durationMs = mClock.getElapsedSinceBootMillis() - mTimeLastTrigger;
399             mWifiMetrics.setWatchdogSuccessTimeDurationMs(durationMs);
400         }
401         // If the watchdog trigger was disabled (it triggered), connecting means we did
402         // something right, re-enable it so it can fire again.
403         localLog("connectedStateTransition: setWatchdogTriggerEnabled to true");
404         setWatchdogTriggerEnabled(true);
405     }
406 
407     /**
408      * Helper function to check if device connected to BSSID
409      * which is in BSSID failure list after watchdog trigger.
410      */
checkIfConnectedBssidHasEverFailed(@onNull WifiInfo wifiInfo)411     private boolean checkIfConnectedBssidHasEverFailed(@NonNull WifiInfo wifiInfo) {
412         return mBssidFailureList.contains(wifiInfo.getBSSID());
413     }
414 
415     /**
416      * Helper function to check if device connect back to same
417      * SSID after watchdog trigger
418      */
checkIfConnectedBackToSameSsid(@onNull WifiInfo wifiInfo)419     private boolean checkIfConnectedBackToSameSsid(@NonNull WifiInfo wifiInfo) {
420         if (TextUtils.equals(mSsidLastTrigger, wifiInfo.getSSID())) {
421             return true;
422         }
423         localLog("checkIfConnectedBackToSameSsid: different SSID be connected");
424         return false;
425     }
426 
427     /**
428      * Triggers a wifi specific bugreport with a based on the current trigger probability.
429      * @param bugDetail description of the bug
430      */
takeBugReportWithCurrentProbability(String bugDetail)431     private void takeBugReportWithCurrentProbability(String bugDetail) {
432         if (mBugReportProbability <= Math.random()) {
433             return;
434         }
435         mHandler.post(() -> mWifiDiagnostics.takeBugReport(BUGREPORT_TITLE, bugDetail));
436     }
437 
438     /**
439      * Increments the failure reason count for the given network, in 'mSsidFailureCount'
440      * Failures are counted per SSID, either; by using the ssid string when the bssid is "any"
441      * or by looking up the ssid attached to a specific bssid
442      * An unused set of counts is also kept which is bssid specific, in 'mRecentAvailableNetworks'
443      * @param ssid of the network that has failed connection
444      * @param bssid of the network that has failed connection, can be "any"
445      * @param reason Message id from ClientModeImpl for this failure
446      */
updateFailureCountForNetwork(String ssid, String bssid, int reason)447     private void updateFailureCountForNetwork(String ssid, String bssid, int reason) {
448         logv("updateFailureCountForNetwork: [" + ssid + ", " + bssid + ", "
449                 + reason + "]");
450         if (BSSID_ANY.equals(bssid)) {
451             incrementSsidFailureCount(ssid, reason);
452         } else {
453             // Bssid count is actually unused except for logging purposes
454             // SSID count is incremented within the BSSID counting method
455             incrementBssidFailureCount(ssid, bssid, reason);
456             mBssidFailureList.add(bssid);
457         }
458     }
459 
460     /**
461      * Update the per-SSID failure count
462      * @param ssid the ssid to increment failure count for
463      * @param reason the failure type to increment count for
464      */
incrementSsidFailureCount(String ssid, int reason)465     private void incrementSsidFailureCount(String ssid, int reason) {
466         Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
467         if (ssidFails == null) {
468             Log.d(TAG, "updateFailureCountForNetwork: No networks for ssid = " + ssid);
469             return;
470         }
471         AvailableNetworkFailureCount failureCount = ssidFails.first;
472         failureCount.incrementFailureCount(reason);
473     }
474 
475     /**
476      * Update the per-BSSID failure count
477      * @param bssid the bssid to increment failure count for
478      * @param reason the failure type to increment count for
479      */
incrementBssidFailureCount(String ssid, String bssid, int reason)480     private void incrementBssidFailureCount(String ssid, String bssid, int reason) {
481         AvailableNetworkFailureCount availableNetworkFailureCount =
482                 mRecentAvailableNetworks.get(bssid);
483         if (availableNetworkFailureCount == null) {
484             Log.d(TAG, "updateFailureCountForNetwork: Unable to find Network [" + ssid
485                     + ", " + bssid + "]");
486             return;
487         }
488         if (!availableNetworkFailureCount.ssid.equals(ssid)) {
489             Log.d(TAG, "updateFailureCountForNetwork: Failed connection attempt has"
490                     + " wrong ssid. Failed [" + ssid + ", " + bssid + "], buffered ["
491                     + availableNetworkFailureCount.ssid + ", " + bssid + "]");
492             return;
493         }
494         if (availableNetworkFailureCount.config == null) {
495             if (mVerboseLoggingEnabled) {
496                 Log.v(TAG, "updateFailureCountForNetwork: network has no config ["
497                         + ssid + ", " + bssid + "]");
498             }
499         }
500         availableNetworkFailureCount.incrementFailureCount(reason);
501         incrementSsidFailureCount(ssid, reason);
502     }
503 
504     /**
505      * Helper function to check if we should ignore BSSID update.
506      * @param bssid BSSID of the access point
507      * @return true if we should ignore BSSID update
508      */
shouldIgnoreBssidUpdate(String bssid)509     public boolean shouldIgnoreBssidUpdate(String bssid) {
510         return mWatchdogAllowedToTrigger
511                 && isBssidOnlyApOfSsid(bssid)
512                 && isSingleSsidRecorded()
513                 && checkIfAtleastOneNetworkHasEverConnected();
514     }
515 
516     /**
517      * Helper function to check if we should ignore SSID update.
518      * @return true if should ignore SSID update
519      */
shouldIgnoreSsidUpdate()520     public boolean shouldIgnoreSsidUpdate() {
521         return mWatchdogAllowedToTrigger
522                 && isSingleSsidRecorded()
523                 && checkIfAtleastOneNetworkHasEverConnected();
524     }
525 
526     /**
527      * Check the specified BSSID is the only BSSID for its corresponding SSID.
528      * @param bssid BSSID of the access point
529      * @return true if only BSSID for its corresponding SSID be observed
530      */
isBssidOnlyApOfSsid(String bssid)531     public boolean isBssidOnlyApOfSsid(String bssid) {
532         AvailableNetworkFailureCount availableNetworkFailureCount =
533                 mRecentAvailableNetworks.get(bssid);
534         if (availableNetworkFailureCount == null) {
535             return false;
536         }
537         String ssid = availableNetworkFailureCount.ssid;
538         Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
539         if (ssidFails == null) {
540             Log.d(TAG, "isOnlyBssidAvailable: Could not find SSID count for " + ssid);
541             return false;
542         }
543         if (ssidFails.second != 1) {
544             return false;
545         }
546         return true;
547     }
548 
549     /**
550      * Check there is only single SSID be observed.
551      * @return true if only single SSID be observed.
552      */
isSingleSsidRecorded()553     private boolean isSingleSsidRecorded() {
554         return (mSsidFailureCount.size() == 1);
555     }
556 
557     /**
558      * Check trigger condition: For all available networks, have we met a failure threshold for each
559      * of them, and have previously connected to at-least one of the available networks
560      * @return is the trigger condition true
561      */
checkTriggerCondition(boolean isConnected)562     private boolean checkTriggerCondition(boolean isConnected) {
563         if (mVerboseLoggingEnabled) Log.v(TAG, "checkTriggerCondition.");
564         // Don't check Watchdog trigger if wifi is in a connected state
565         // (This should not occur, but we want to protect against any race conditions)
566         if (isConnected) return false;
567         // Don't check Watchdog trigger if trigger is not enabled
568         if (!mWatchdogAllowedToTrigger) return false;
569 
570         for (Map.Entry<String, AvailableNetworkFailureCount> entry
571                 : mRecentAvailableNetworks.entrySet()) {
572             if (!isOverFailureThreshold(entry.getKey())) {
573                 // This available network is not over failure threshold, meaning we still have a
574                 // network to try connecting to
575                 return false;
576             }
577         }
578         // We have met the failure count for every available network.
579         // Trigger restart if there exists at-least one network that we have previously connected.
580         boolean atleastOneNetworkHasEverConnected = checkIfAtleastOneNetworkHasEverConnected();
581         logv("checkTriggerCondition: return = " + atleastOneNetworkHasEverConnected);
582         return checkIfAtleastOneNetworkHasEverConnected();
583     }
584 
checkIfAtleastOneNetworkHasEverConnected()585     private boolean checkIfAtleastOneNetworkHasEverConnected() {
586         for (Map.Entry<String, AvailableNetworkFailureCount> entry
587                 : mRecentAvailableNetworks.entrySet()) {
588             if (entry.getValue().config != null
589                     && entry.getValue().config.getNetworkSelectionStatus().hasEverConnected()) {
590                 return true;
591             }
592         }
593         return false;
594     }
595 
596     /**
597      * Update WifiMetrics with various Watchdog stats (trigger counts, failed network counts)
598      */
incrementWifiMetricsTriggerCounts()599     private void incrementWifiMetricsTriggerCounts() {
600         if (mVerboseLoggingEnabled) Log.v(TAG, "incrementWifiMetricsTriggerCounts.");
601         mWifiMetrics.incrementNumLastResortWatchdogTriggers();
602         mWifiMetrics.addCountToNumLastResortWatchdogAvailableNetworksTotal(
603                 mSsidFailureCount.size());
604         // Number of networks over each failure type threshold, present at trigger time
605         int badAuth = 0;
606         int badAssoc = 0;
607         int badDhcp = 0;
608         int badSum = 0;
609         for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry
610                 : mSsidFailureCount.entrySet()) {
611             badSum = entry.getValue().first.associationRejection
612                     + entry.getValue().first.authenticationFailure
613                     + entry.getValue().first.dhcpFailure;
614             // count as contributor if over half of badSum.
615             if (badSum >= FAILURE_THRESHOLD) {
616                 badAssoc += (entry.getValue().first.associationRejection >= badSum / 2) ? 1 : 0;
617                 badAuth += (entry.getValue().first.authenticationFailure >= badSum / 2) ? 1 : 0;
618                 badDhcp += (entry.getValue().first.dhcpFailure >= badSum / 2) ? 1 : 0;
619             }
620         }
621         if (badAuth > 0) {
622             mWifiMetrics.addCountToNumLastResortWatchdogBadAuthenticationNetworksTotal(badAuth);
623             mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAuthentication();
624         }
625         if (badAssoc > 0) {
626             mWifiMetrics.addCountToNumLastResortWatchdogBadAssociationNetworksTotal(badAssoc);
627             mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadAssociation();
628         }
629         if (badDhcp > 0) {
630             mWifiMetrics.addCountToNumLastResortWatchdogBadDhcpNetworksTotal(badDhcp);
631             mWifiMetrics.incrementNumLastResortWatchdogTriggersWithBadDhcp();
632         }
633     }
634 
635     /**
636      * Clear all failure counts
637      */
clearAllFailureCounts()638     public void clearAllFailureCounts() {
639         if (mVerboseLoggingEnabled) Log.v(TAG, "clearAllFailureCounts.");
640         for (Map.Entry<String, AvailableNetworkFailureCount> entry
641                 : mRecentAvailableNetworks.entrySet()) {
642             final AvailableNetworkFailureCount failureCount = entry.getValue();
643             failureCount.resetCounts();
644         }
645         for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry
646                 : mSsidFailureCount.entrySet()) {
647             final AvailableNetworkFailureCount failureCount = entry.getValue().first;
648             failureCount.resetCounts();
649         }
650         mBssidFailureList.clear();
651     }
652     /**
653      * Gets the buffer of recently available networks
654      */
655     @VisibleForTesting
getRecentAvailableNetworks()656     Map<String, AvailableNetworkFailureCount> getRecentAvailableNetworks() {
657         return mRecentAvailableNetworks;
658     }
659 
660     /**
661      * Activates or deactivates the Watchdog trigger. Counting and network buffering still occurs
662      * @param enable true to enable the Watchdog trigger, false to disable it
663      */
setWatchdogTriggerEnabled(boolean enable)664     private void setWatchdogTriggerEnabled(boolean enable) {
665         if (mVerboseLoggingEnabled) Log.v(TAG, "setWatchdogTriggerEnabled: enable = " + enable);
666         // Reset failure counts before actives watchdog
667         if (enable) {
668             clearAllFailureCounts();
669         }
670         mWatchdogAllowedToTrigger = enable;
671     }
672 
673     /**
674      * Prints all networks & counts within mRecentAvailableNetworks to string
675      */
676     @Override
toString()677     public String toString() {
678         StringBuilder sb = new StringBuilder();
679         sb.append("mWatchdogFeatureEnabled: ").append(getWifiWatchdogFeature());
680         sb.append("\nmWatchdogAllowedToTrigger: ").append(mWatchdogAllowedToTrigger);
681         sb.append("\nmRecentAvailableNetworks: ").append(mRecentAvailableNetworks.size());
682         for (Map.Entry<String, AvailableNetworkFailureCount> entry
683                 : mRecentAvailableNetworks.entrySet()) {
684             sb.append("\n ").append(entry.getKey()).append(": ").append(entry.getValue())
685                 .append(", Age: ").append(entry.getValue().age);
686         }
687         sb.append("\nmSsidFailureCount:");
688         for (Map.Entry<String, Pair<AvailableNetworkFailureCount, Integer>> entry :
689                 mSsidFailureCount.entrySet()) {
690             final AvailableNetworkFailureCount failureCount = entry.getValue().first;
691             final Integer apCount = entry.getValue().second;
692             sb.append("\n").append(entry.getKey()).append(": ").append(apCount).append(",")
693                     .append(failureCount.toString());
694         }
695         return sb.toString();
696     }
697 
698     /**
699      * @param bssid bssid to check the failures for
700      * @return true if sum of failure count is over FAILURE_THRESHOLD
701      */
702     @VisibleForTesting
isOverFailureThreshold(String bssid)703     boolean isOverFailureThreshold(String bssid) {
704         return (getFailureCount(bssid, FAILURE_CODE_ASSOCIATION)
705                 + getFailureCount(bssid, FAILURE_CODE_AUTHENTICATION)
706                 + getFailureCount(bssid, FAILURE_CODE_DHCP)) >= FAILURE_THRESHOLD;
707     }
708 
709     /**
710      * Get the failure count for a specific bssid. This actually checks the ssid attached to the
711      * BSSID and returns the SSID count
712      * @param reason failure reason to get count for
713      */
714     @VisibleForTesting
getFailureCount(String bssid, int reason)715     int getFailureCount(String bssid, int reason) {
716         AvailableNetworkFailureCount availableNetworkFailureCount =
717                 mRecentAvailableNetworks.get(bssid);
718         if (availableNetworkFailureCount == null) {
719             return 0;
720         }
721         String ssid = availableNetworkFailureCount.ssid;
722         Pair<AvailableNetworkFailureCount, Integer> ssidFails = mSsidFailureCount.get(ssid);
723         if (ssidFails == null) {
724             Log.d(TAG, "getFailureCount: Could not find SSID count for " + ssid);
725             return 0;
726         }
727         final AvailableNetworkFailureCount failCount = ssidFails.first;
728         switch (reason) {
729             case FAILURE_CODE_ASSOCIATION:
730                 return failCount.associationRejection;
731             case FAILURE_CODE_AUTHENTICATION:
732                 return failCount.authenticationFailure;
733             case FAILURE_CODE_DHCP:
734                 return failCount.dhcpFailure;
735             default:
736                 return 0;
737         }
738     }
739 
740     /**
741      * Sets whether wifi watchdog should trigger recovery
742      */
setWifiWatchdogFeature(boolean enable)743     public void setWifiWatchdogFeature(boolean enable) {
744         logv("setWifiWatchdogFeature: " + enable);
745         mWatchdogFeatureEnabled = enable;
746         // for debugging purpose, reset mWatchdogAllowedToTrigger as well
747         setWatchdogTriggerEnabled(true);
748     }
749 
750     /**
751      * Returns whether wifi watchdog should trigger recovery.
752      */
getWifiWatchdogFeature()753     public boolean getWifiWatchdogFeature() {
754         if (mWatchdogFeatureEnabled == null) {
755             mWatchdogFeatureEnabled = mContext.getResources().getBoolean(
756                     R.bool.config_wifi_watchdog_enabled);
757         }
758         return mWatchdogFeatureEnabled;
759     }
760 
761     /** Enable/disable verbose logging. */
enableVerboseLogging(int verbose)762     public void enableVerboseLogging(int verbose) {
763         mVerboseLoggingEnabled = verbose > 0;
764     }
765 
766     @VisibleForTesting
setBugReportProbability(double newProbability)767     void setBugReportProbability(double newProbability) {
768         mBugReportProbability = newProbability;
769     }
770 
771     /**
772      * This class holds the failure counts for an 'available network' (one of the potential
773      * candidates for connection, as determined by framework).
774      */
775     public static class AvailableNetworkFailureCount {
776         /**
777          * WifiConfiguration associated with this network. Can be null for Ephemeral networks
778          */
779         public WifiConfiguration config;
780         /**
781         * SSID of the network (from ScanDetail)
782         */
783         public String ssid = "";
784         /**
785          * Number of times network has failed due to Association Rejection
786          */
787         public int associationRejection = 0;
788         /**
789          * Number of times network has failed due to Authentication Failure or SSID_TEMP_DISABLED
790          */
791         public int authenticationFailure = 0;
792         /**
793          * Number of times network has failed due to DHCP failure
794          */
795         public int dhcpFailure = 0;
796         /**
797          * Number of scanResults since this network was last seen
798          */
799         public int age = 0;
800 
AvailableNetworkFailureCount(WifiConfiguration configParam)801         AvailableNetworkFailureCount(WifiConfiguration configParam) {
802             this.config = configParam;
803         }
804 
805         /**
806          * @param reason failure reason to increment count for
807          */
incrementFailureCount(int reason)808         public void incrementFailureCount(int reason) {
809             switch (reason) {
810                 case FAILURE_CODE_ASSOCIATION:
811                     associationRejection++;
812                     break;
813                 case FAILURE_CODE_AUTHENTICATION:
814                     authenticationFailure++;
815                     break;
816                 case FAILURE_CODE_DHCP:
817                     dhcpFailure++;
818                     break;
819                 default: //do nothing
820             }
821         }
822 
823         /**
824          * Set all failure counts for this network to 0
825          */
resetCounts()826         void resetCounts() {
827             associationRejection = 0;
828             authenticationFailure = 0;
829             dhcpFailure = 0;
830         }
831 
toString()832         public String toString() {
833             return  ssid + " HasEverConnected: " + ((config != null)
834                     ? config.getNetworkSelectionStatus().hasEverConnected() : "null_config")
835                     + ", Failures: {"
836                     + "Assoc: " + associationRejection
837                     + ", Auth: " + authenticationFailure
838                     + ", Dhcp: " + dhcpFailure
839                     + "}";
840         }
841     }
842 
843     /**
844      * Helper function for logging into local log buffer.
845      */
localLog(String s)846     private void localLog(String s) {
847         mLocalLog.log(s);
848     }
849 
logv(String s)850     private void logv(String s) {
851         mLocalLog.log(s);
852         if (mVerboseLoggingEnabled) {
853             Log.v(TAG, s);
854         }
855     }
856 
loge(String s)857     private void loge(String s) {
858         mLocalLog.log(s);
859         Log.e(TAG, s);
860     }
861 
862     /**
863      * Dump the local log buffer and other internal state of WifiLastResortWatchdog.
864      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)865     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
866         pw.println("Dump of WifiLastResortWatchdog");
867         pw.println("WifiLastResortWatchdog - Log Begin ----");
868         mLocalLog.dump(fd, pw, args);
869         pw.println("WifiLastResortWatchdog - Log End ----");
870     }
871 }
872