• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.content.Context;
23 import android.net.wifi.ScanResult;
24 import android.net.wifi.WifiManager;
25 import android.util.ArrayMap;
26 import android.util.ArraySet;
27 import android.util.LocalLog;
28 import android.util.Log;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.wifi.resources.R;
32 
33 import java.io.FileDescriptor;
34 import java.io.PrintWriter;
35 import java.lang.annotation.Retention;
36 import java.lang.annotation.RetentionPolicy;
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.Calendar;
40 import java.util.Collections;
41 import java.util.LinkedList;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Set;
45 import java.util.concurrent.TimeUnit;
46 import java.util.stream.Collectors;
47 import java.util.stream.Stream;
48 
49 /**
50  * This class manages the addition and removal of BSSIDs to the BSSID blocklist, which is used
51  * for firmware roaming and network selection.
52  */
53 public class BssidBlocklistMonitor {
54     // A special type association rejection
55     public static final int REASON_AP_UNABLE_TO_HANDLE_NEW_STA = 0;
56     // No internet
57     public static final int REASON_NETWORK_VALIDATION_FAILURE = 1;
58     // Wrong password error
59     public static final int REASON_WRONG_PASSWORD = 2;
60     // Incorrect EAP credentials
61     public static final int REASON_EAP_FAILURE = 3;
62     // Other association rejection failures
63     public static final int REASON_ASSOCIATION_REJECTION = 4;
64     // Association timeout failures.
65     public static final int REASON_ASSOCIATION_TIMEOUT = 5;
66     // Other authentication failures
67     public static final int REASON_AUTHENTICATION_FAILURE = 6;
68     // DHCP failures
69     public static final int REASON_DHCP_FAILURE = 7;
70     // Abnormal disconnect error
71     public static final int REASON_ABNORMAL_DISCONNECT = 8;
72     // AP initiated disconnect for a given duration.
73     public static final int REASON_FRAMEWORK_DISCONNECT_MBO_OCE = 9;
74     // Avoid connecting to the failed AP when trying to reconnect on other available candidates.
75     public static final int REASON_FRAMEWORK_DISCONNECT_FAST_RECONNECT = 10;
76     // The connected scorer has disconnected this network.
77     public static final int REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE = 11;
78     // Constant being used to keep track of how many failure reasons there are.
79     public static final int NUMBER_REASON_CODES = 12;
80     public static final int INVALID_REASON = -1;
81 
82     @IntDef(prefix = { "REASON_" }, value = {
83             REASON_AP_UNABLE_TO_HANDLE_NEW_STA,
84             REASON_NETWORK_VALIDATION_FAILURE,
85             REASON_WRONG_PASSWORD,
86             REASON_EAP_FAILURE,
87             REASON_ASSOCIATION_REJECTION,
88             REASON_ASSOCIATION_TIMEOUT,
89             REASON_AUTHENTICATION_FAILURE,
90             REASON_DHCP_FAILURE,
91             REASON_ABNORMAL_DISCONNECT,
92             REASON_FRAMEWORK_DISCONNECT_MBO_OCE,
93             REASON_FRAMEWORK_DISCONNECT_FAST_RECONNECT,
94             REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE
95     })
96     @Retention(RetentionPolicy.SOURCE)
97     public @interface FailureReason {}
98 
99     // To be filled with values from the overlay.
100     private static final int[] FAILURE_COUNT_DISABLE_THRESHOLD = new int[NUMBER_REASON_CODES];
101     private boolean mFailureCountDisableThresholdArrayInitialized = false;
102     private static final String[] FAILURE_REASON_STRINGS = {
103             "REASON_AP_UNABLE_TO_HANDLE_NEW_STA",
104             "REASON_NETWORK_VALIDATION_FAILURE",
105             "REASON_WRONG_PASSWORD",
106             "REASON_EAP_FAILURE",
107             "REASON_ASSOCIATION_REJECTION",
108             "REASON_ASSOCIATION_TIMEOUT",
109             "REASON_AUTHENTICATION_FAILURE",
110             "REASON_DHCP_FAILURE",
111             "REASON_ABNORMAL_DISCONNECT",
112             "REASON_FRAMEWORK_DISCONNECT_MBO_OCE",
113             "REASON_FRAMEWORK_DISCONNECT_FAST_RECONNECT",
114             "REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE"
115     };
116     private static final Set<Integer> LOW_RSSI_SENSITIVE_FAILURES = new ArraySet<>(Arrays.asList(
117             REASON_NETWORK_VALIDATION_FAILURE,
118             REASON_EAP_FAILURE,
119             REASON_ASSOCIATION_REJECTION,
120             REASON_ASSOCIATION_TIMEOUT,
121             REASON_AUTHENTICATION_FAILURE,
122             REASON_DHCP_FAILURE,
123             REASON_ABNORMAL_DISCONNECT,
124             REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE
125     ));
126     private static final long ABNORMAL_DISCONNECT_RESET_TIME_MS = TimeUnit.HOURS.toMillis(3);
127     private static final int MIN_RSSI_DIFF_TO_UNBLOCK_BSSID = 5;
128     private static final String TAG = "BssidBlocklistMonitor";
129 
130     private final Context mContext;
131     private final WifiLastResortWatchdog mWifiLastResortWatchdog;
132     private final WifiConnectivityHelper mConnectivityHelper;
133     private final Clock mClock;
134     private final LocalLog mLocalLog;
135     private final Calendar mCalendar;
136     private final WifiScoreCard mWifiScoreCard;
137     private final ScoringParams mScoringParams;
138 
139     // Map of bssid to BssidStatus
140     private Map<String, BssidStatus> mBssidStatusMap = new ArrayMap<>();
141 
142     // Keeps history of 30 blocked BSSIDs that were most recently removed.
143     private BssidStatusHistoryLogger mBssidStatusHistoryLogger = new BssidStatusHistoryLogger(30);
144 
145     /**
146      * Create a new instance of BssidBlocklistMonitor
147      */
BssidBlocklistMonitor(Context context, WifiConnectivityHelper connectivityHelper, WifiLastResortWatchdog wifiLastResortWatchdog, Clock clock, LocalLog localLog, WifiScoreCard wifiScoreCard, ScoringParams scoringParams)148     BssidBlocklistMonitor(Context context, WifiConnectivityHelper connectivityHelper,
149             WifiLastResortWatchdog wifiLastResortWatchdog, Clock clock, LocalLog localLog,
150             WifiScoreCard wifiScoreCard, ScoringParams scoringParams) {
151         mContext = context;
152         mConnectivityHelper = connectivityHelper;
153         mWifiLastResortWatchdog = wifiLastResortWatchdog;
154         mClock = clock;
155         mLocalLog = localLog;
156         mCalendar = Calendar.getInstance();
157         mWifiScoreCard = wifiScoreCard;
158         mScoringParams = scoringParams;
159     }
160 
161     // A helper to log debugging information in the local log buffer, which can
162     // be retrieved in bugreport.
localLog(String log)163     private void localLog(String log) {
164         mLocalLog.log(log);
165     }
166 
167     /**
168      * calculates the blocklist duration based on the current failure streak with exponential
169      * backoff.
170      * @param failureStreak should be greater or equal to 0.
171      * @return duration to block the BSSID in milliseconds
172      */
getBlocklistDurationWithExponentialBackoff(int failureStreak, int baseBlocklistDurationMs)173     private long getBlocklistDurationWithExponentialBackoff(int failureStreak,
174             int baseBlocklistDurationMs) {
175         failureStreak = Math.min(failureStreak, mContext.getResources().getInteger(
176                 R.integer.config_wifiBssidBlocklistMonitorFailureStreakCap));
177         if (failureStreak < 1) {
178             return baseBlocklistDurationMs;
179         }
180         return (long) (Math.pow(2.0, (double) failureStreak) * baseBlocklistDurationMs);
181     }
182 
183     /**
184      * Dump the local log buffer and other internal state of BssidBlocklistMonitor.
185      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)186     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
187         pw.println("Dump of BssidBlocklistMonitor");
188         pw.println("BssidBlocklistMonitor - Bssid blocklist begin ----");
189         mBssidStatusMap.values().stream().forEach(entry -> pw.println(entry));
190         pw.println("BssidBlocklistMonitor - Bssid blocklist end ----");
191         mBssidStatusHistoryLogger.dump(pw);
192     }
193 
addToBlocklist(@onNull BssidStatus entry, long durationMs, @FailureReason int reason, int rssi)194     private void addToBlocklist(@NonNull BssidStatus entry, long durationMs,
195             @FailureReason int reason, int rssi) {
196         entry.setAsBlocked(durationMs, reason, rssi);
197         localLog(TAG + " addToBlocklist: bssid=" + entry.bssid + ", ssid=" + entry.ssid
198                 + ", durationMs=" + durationMs + ", reason=" + getFailureReasonString(reason)
199                 + ", rssi=" + rssi);
200     }
201 
202     /**
203      * increments the number of failures for the given bssid and returns the number of failures so
204      * far.
205      * @return the BssidStatus for the BSSID
206      */
incrementFailureCountForBssid( @onNull String bssid, @NonNull String ssid, int reasonCode)207     private @NonNull BssidStatus incrementFailureCountForBssid(
208             @NonNull String bssid, @NonNull String ssid, int reasonCode) {
209         BssidStatus status = getOrCreateBssidStatus(bssid, ssid);
210         status.incrementFailureCount(reasonCode);
211         return status;
212     }
213 
214     /**
215      * Get the BssidStatus representing the BSSID or create a new one if it doesn't exist.
216      */
getOrCreateBssidStatus(@onNull String bssid, @NonNull String ssid)217     private @NonNull BssidStatus getOrCreateBssidStatus(@NonNull String bssid,
218             @NonNull String ssid) {
219         BssidStatus status = mBssidStatusMap.get(bssid);
220         if (status == null || !ssid.equals(status.ssid)) {
221             if (status != null) {
222                 localLog("getOrCreateBssidStatus: BSSID=" + bssid + ", SSID changed from "
223                         + status.ssid + " to " + ssid);
224             }
225             status = new BssidStatus(bssid, ssid);
226             mBssidStatusMap.put(bssid, status);
227         }
228         return status;
229     }
230 
isValidNetworkAndFailureReason(String bssid, String ssid, @FailureReason int reasonCode)231     private boolean isValidNetworkAndFailureReason(String bssid, String ssid,
232             @FailureReason int reasonCode) {
233         if (bssid == null || ssid == null || WifiManager.UNKNOWN_SSID.equals(ssid)
234                 || bssid.equals(ClientModeImpl.SUPPLICANT_BSSID_ANY)
235                 || reasonCode < 0 || reasonCode >= NUMBER_REASON_CODES) {
236             Log.e(TAG, "Invalid input: BSSID=" + bssid + ", SSID=" + ssid
237                     + ", reasonCode=" + reasonCode);
238             return false;
239         }
240         return true;
241     }
242 
shouldWaitForWatchdogToTriggerFirst(String bssid, @FailureReason int reasonCode)243     private boolean shouldWaitForWatchdogToTriggerFirst(String bssid,
244             @FailureReason int reasonCode) {
245         boolean isWatchdogRelatedFailure = reasonCode == REASON_ASSOCIATION_REJECTION
246                 || reasonCode == REASON_AUTHENTICATION_FAILURE
247                 || reasonCode == REASON_DHCP_FAILURE;
248         return isWatchdogRelatedFailure && mWifiLastResortWatchdog.shouldIgnoreBssidUpdate(bssid);
249     }
250 
251     /**
252      * Block any attempts to auto-connect to the BSSID for the specified duration.
253      * This is meant to be used by features that need wifi to avoid a BSSID for a certain duration,
254      * and thus will not increase the failure streak counters.
255      * @param bssid identifies the AP to block.
256      * @param ssid identifies the SSID the AP belongs to.
257      * @param durationMs duration in millis to block.
258      * @param blockReason reason for blocking the BSSID.
259      * @param rssi the latest RSSI observed.
260      */
blockBssidForDurationMs(@onNull String bssid, @NonNull String ssid, long durationMs, @FailureReason int blockReason, int rssi)261     public void blockBssidForDurationMs(@NonNull String bssid, @NonNull String ssid,
262             long durationMs, @FailureReason int blockReason, int rssi) {
263         if (durationMs <= 0 || !isValidNetworkAndFailureReason(bssid, ssid, blockReason)) {
264             Log.e(TAG, "Invalid input: BSSID=" + bssid + ", SSID=" + ssid
265                     + ", durationMs=" + durationMs + ", blockReason=" + blockReason
266                     + ", rssi=" + rssi);
267             return;
268         }
269         BssidStatus status = getOrCreateBssidStatus(bssid, ssid);
270         if (status.isInBlocklist
271                 && status.blocklistEndTimeMs - mClock.getWallClockMillis() > durationMs) {
272             // Return because this BSSID is already being blocked for a longer time.
273             return;
274         }
275         addToBlocklist(status, durationMs, blockReason, rssi);
276     }
277 
getFailureReasonString(@ailureReason int reasonCode)278     private String getFailureReasonString(@FailureReason int reasonCode) {
279         if (reasonCode == INVALID_REASON) {
280             return "INVALID_REASON";
281         } else if (reasonCode < 0 || reasonCode >= FAILURE_REASON_STRINGS.length) {
282             return "REASON_UNKNOWN";
283         }
284         return FAILURE_REASON_STRINGS[reasonCode];
285     }
286 
getFailureThresholdForReason(@ailureReason int reasonCode)287     private int getFailureThresholdForReason(@FailureReason int reasonCode) {
288         if (mFailureCountDisableThresholdArrayInitialized) {
289             return FAILURE_COUNT_DISABLE_THRESHOLD[reasonCode];
290         }
291         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_AP_UNABLE_TO_HANDLE_NEW_STA] =
292                 mContext.getResources().getInteger(
293                         R.integer.config_wifiBssidBlocklistMonitorApUnableToHandleNewStaThreshold);
294         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_NETWORK_VALIDATION_FAILURE] =
295                 mContext.getResources().getInteger(R.integer
296                         .config_wifiBssidBlocklistMonitorNetworkValidationFailureThreshold);
297         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_WRONG_PASSWORD] =
298                 mContext.getResources().getInteger(
299                         R.integer.config_wifiBssidBlocklistMonitorWrongPasswordThreshold);
300         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_EAP_FAILURE] =
301                 mContext.getResources().getInteger(
302                         R.integer.config_wifiBssidBlocklistMonitorEapFailureThreshold);
303         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_ASSOCIATION_REJECTION] =
304                 mContext.getResources().getInteger(
305                         R.integer.config_wifiBssidBlocklistMonitorAssociationRejectionThreshold);
306         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_ASSOCIATION_TIMEOUT] =
307                 mContext.getResources().getInteger(
308                         R.integer.config_wifiBssidBlocklistMonitorAssociationTimeoutThreshold);
309         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_AUTHENTICATION_FAILURE] =
310                 mContext.getResources().getInteger(
311                         R.integer.config_wifiBssidBlocklistMonitorAuthenticationFailureThreshold);
312         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_DHCP_FAILURE] =
313                 mContext.getResources().getInteger(
314                         R.integer.config_wifiBssidBlocklistMonitorDhcpFailureThreshold);
315         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_ABNORMAL_DISCONNECT] =
316                 mContext.getResources().getInteger(
317                         R.integer.config_wifiBssidBlocklistMonitorAbnormalDisconnectThreshold);
318         mFailureCountDisableThresholdArrayInitialized = true;
319         return FAILURE_COUNT_DISABLE_THRESHOLD[reasonCode];
320     }
321 
handleBssidConnectionFailureInternal(String bssid, String ssid, @FailureReason int reasonCode, int rssi)322     private boolean handleBssidConnectionFailureInternal(String bssid, String ssid,
323             @FailureReason int reasonCode, int rssi) {
324         BssidStatus entry = incrementFailureCountForBssid(bssid, ssid, reasonCode);
325         int failureThreshold = getFailureThresholdForReason(reasonCode);
326         int currentStreak = mWifiScoreCard.getBssidBlocklistStreak(ssid, bssid, reasonCode);
327         if (currentStreak > 0 || entry.failureCount[reasonCode] >= failureThreshold) {
328             // To rule out potential device side issues, don't add to blocklist if
329             // WifiLastResortWatchdog is still not triggered
330             if (shouldWaitForWatchdogToTriggerFirst(bssid, reasonCode)) {
331                 return false;
332             }
333             int baseBlockDurationMs = getBaseBlockDurationForReason(reasonCode);
334             addToBlocklist(entry,
335                     getBlocklistDurationWithExponentialBackoff(currentStreak, baseBlockDurationMs),
336                     reasonCode, rssi);
337             mWifiScoreCard.incrementBssidBlocklistStreak(ssid, bssid, reasonCode);
338             return true;
339         }
340         return false;
341     }
342 
getBaseBlockDurationForReason(int blockReason)343     private int getBaseBlockDurationForReason(int blockReason) {
344         switch (blockReason) {
345             case REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE:
346                 return mContext.getResources().getInteger(R.integer
347                         .config_wifiBssidBlocklistMonitorConnectedScoreBaseBlockDurationMs);
348             default:
349                 return mContext.getResources().getInteger(
350                         R.integer.config_wifiBssidBlocklistMonitorBaseBlockDurationMs);
351         }
352     }
353 
354     /**
355      * Note a failure event on a bssid and perform appropriate actions.
356      * @return True if the blocklist has been modified.
357      */
handleBssidConnectionFailure(String bssid, String ssid, @FailureReason int reasonCode, int rssi)358     public boolean handleBssidConnectionFailure(String bssid, String ssid,
359             @FailureReason int reasonCode, int rssi) {
360         if (!isValidNetworkAndFailureReason(bssid, ssid, reasonCode)) {
361             return false;
362         }
363         if (reasonCode == REASON_ABNORMAL_DISCONNECT) {
364             long connectionTime = mWifiScoreCard.getBssidConnectionTimestampMs(ssid, bssid);
365             // only count disconnects that happen shortly after a connection.
366             if (mClock.getWallClockMillis() - connectionTime
367                     > mContext.getResources().getInteger(
368                             R.integer.config_wifiBssidBlocklistAbnormalDisconnectTimeWindowMs)) {
369                 return false;
370             }
371         }
372         return handleBssidConnectionFailureInternal(bssid, ssid, reasonCode, rssi);
373     }
374 
375     /**
376      * Note a connection success event on a bssid and clear appropriate failure counters.
377      */
handleBssidConnectionSuccess(@onNull String bssid, @NonNull String ssid)378     public void handleBssidConnectionSuccess(@NonNull String bssid, @NonNull String ssid) {
379         /**
380          * First reset the blocklist streak.
381          * This needs to be done even if a BssidStatus is not found, since the BssidStatus may
382          * have been removed due to blocklist timeout.
383          */
384         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_AP_UNABLE_TO_HANDLE_NEW_STA);
385         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_WRONG_PASSWORD);
386         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_EAP_FAILURE);
387         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_ASSOCIATION_REJECTION);
388         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_ASSOCIATION_TIMEOUT);
389         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_AUTHENTICATION_FAILURE);
390 
391         long connectionTime = mClock.getWallClockMillis();
392         long prevConnectionTime = mWifiScoreCard.setBssidConnectionTimestampMs(
393                 ssid, bssid, connectionTime);
394         if (connectionTime - prevConnectionTime > ABNORMAL_DISCONNECT_RESET_TIME_MS) {
395             mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_ABNORMAL_DISCONNECT);
396             mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid,
397                     REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE);
398         }
399 
400         BssidStatus status = mBssidStatusMap.get(bssid);
401         if (status == null) {
402             return;
403         }
404         // Clear the L2 failure counters
405         status.failureCount[REASON_AP_UNABLE_TO_HANDLE_NEW_STA] = 0;
406         status.failureCount[REASON_WRONG_PASSWORD] = 0;
407         status.failureCount[REASON_EAP_FAILURE] = 0;
408         status.failureCount[REASON_ASSOCIATION_REJECTION] = 0;
409         status.failureCount[REASON_ASSOCIATION_TIMEOUT] = 0;
410         status.failureCount[REASON_AUTHENTICATION_FAILURE] = 0;
411         if (connectionTime - prevConnectionTime > ABNORMAL_DISCONNECT_RESET_TIME_MS) {
412             status.failureCount[REASON_ABNORMAL_DISCONNECT] = 0;
413         }
414     }
415 
416     /**
417      * Note a successful network validation on a BSSID and clear appropriate failure counters.
418      * And then remove the BSSID from blocklist.
419      */
handleNetworkValidationSuccess(@onNull String bssid, @NonNull String ssid)420     public void handleNetworkValidationSuccess(@NonNull String bssid, @NonNull String ssid) {
421         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_NETWORK_VALIDATION_FAILURE);
422         BssidStatus status = mBssidStatusMap.get(bssid);
423         if (status == null) {
424             return;
425         }
426         status.failureCount[REASON_NETWORK_VALIDATION_FAILURE] = 0;
427         /**
428          * Network validation may take more than 1 tries to succeed.
429          * remove the BSSID from blocklist to make sure we are not accidentally blocking good
430          * BSSIDs.
431          **/
432         if (status.isInBlocklist) {
433             mBssidStatusHistoryLogger.add(status, "Network validation success");
434             mBssidStatusMap.remove(bssid);
435         }
436     }
437 
438     /**
439      * Note a successful DHCP provisioning and clear appropriate faliure counters.
440      */
handleDhcpProvisioningSuccess(@onNull String bssid, @NonNull String ssid)441     public void handleDhcpProvisioningSuccess(@NonNull String bssid, @NonNull String ssid) {
442         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_DHCP_FAILURE);
443         BssidStatus status = mBssidStatusMap.get(bssid);
444         if (status == null) {
445             return;
446         }
447         status.failureCount[REASON_DHCP_FAILURE] = 0;
448     }
449 
450     /**
451      * Note the removal of a network from the Wifi stack's internal database and reset
452      * appropriate failure counters.
453      * @param ssid
454      */
handleNetworkRemoved(@onNull String ssid)455     public void handleNetworkRemoved(@NonNull String ssid) {
456         clearBssidBlocklistForSsid(ssid);
457         mWifiScoreCard.resetBssidBlocklistStreakForSsid(ssid);
458     }
459 
460     /**
461      * Clears the blocklist for BSSIDs associated with the input SSID only.
462      * @param ssid
463      */
clearBssidBlocklistForSsid(@onNull String ssid)464     public void clearBssidBlocklistForSsid(@NonNull String ssid) {
465         int prevSize = mBssidStatusMap.size();
466         mBssidStatusMap.entrySet().removeIf(e -> {
467             BssidStatus status = e.getValue();
468             if (status.ssid == null) {
469                 return false;
470             }
471             if (status.ssid.equals(ssid)) {
472                 mBssidStatusHistoryLogger.add(status, "clearBssidBlocklistForSsid");
473                 return true;
474             }
475             return false;
476         });
477         int diff = prevSize - mBssidStatusMap.size();
478         if (diff > 0) {
479             localLog(TAG + " clearBssidBlocklistForSsid: SSID=" + ssid
480                     + ", num BSSIDs cleared=" + diff);
481         }
482     }
483 
484     /**
485      * Clears the BSSID blocklist and failure counters.
486      */
clearBssidBlocklist()487     public void clearBssidBlocklist() {
488         if (mBssidStatusMap.size() > 0) {
489             int prevSize = mBssidStatusMap.size();
490             for (BssidStatus status : mBssidStatusMap.values()) {
491                 mBssidStatusHistoryLogger.add(status, "clearBssidBlocklist");
492             }
493             mBssidStatusMap.clear();
494             localLog(TAG + " clearBssidBlocklist: num BSSIDs cleared="
495                     + (prevSize - mBssidStatusMap.size()));
496         }
497     }
498 
499     /**
500      * @param ssid
501      * @return the number of BSSIDs currently in the blocklist for the |ssid|.
502      */
updateAndGetNumBlockedBssidsForSsid(@onNull String ssid)503     public int updateAndGetNumBlockedBssidsForSsid(@NonNull String ssid) {
504         return (int) updateAndGetBssidBlocklistInternal()
505                 .filter(entry -> ssid.equals(entry.ssid)).count();
506     }
507 
getNumBlockedBssidsForSsid(@ullable String ssid)508     private int getNumBlockedBssidsForSsid(@Nullable String ssid) {
509         if (ssid == null) {
510             return 0;
511         }
512         return (int) mBssidStatusMap.values().stream()
513                 .filter(entry -> entry.isInBlocklist && ssid.equals(entry.ssid))
514                 .count();
515     }
516 
517     /**
518      * Overloaded version of updateAndGetBssidBlocklist.
519      * Accepts a @Nullable String ssid as input, and updates the firmware roaming
520      * configuration if the blocklist for the input ssid has been changed.
521      * @param ssid to update firmware roaming configuration for.
522      * @return Set of BSSIDs currently in the blocklist
523      */
updateAndGetBssidBlocklistForSsid(@ullable String ssid)524     public Set<String> updateAndGetBssidBlocklistForSsid(@Nullable String ssid) {
525         int numBefore = getNumBlockedBssidsForSsid(ssid);
526         Set<String> bssidBlocklist = updateAndGetBssidBlocklist();
527         if (getNumBlockedBssidsForSsid(ssid) != numBefore) {
528             updateFirmwareRoamingConfiguration(ssid);
529         }
530         return bssidBlocklist;
531     }
532 
533     /**
534      * Gets the BSSIDs that are currently in the blocklist.
535      * @return Set of BSSIDs currently in the blocklist
536      */
updateAndGetBssidBlocklist()537     public Set<String> updateAndGetBssidBlocklist() {
538         return updateAndGetBssidBlocklistInternal()
539                 .map(entry -> entry.bssid)
540                 .collect(Collectors.toSet());
541     }
542 
543     /**
544      * Gets the list of block reasons for BSSIDs currently in the blocklist.
545      * @return The set of unique reasons for blocking BSSIDs with this SSID.
546      */
getFailureReasonsForSsid(@onNull String ssid)547     public Set<Integer> getFailureReasonsForSsid(@NonNull String ssid) {
548         if (ssid == null) {
549             return Collections.emptySet();
550         }
551         return mBssidStatusMap.values().stream()
552                 .filter(entry -> entry.isInBlocklist && ssid.equals(entry.ssid))
553                 .map(entry -> entry.blockReason)
554                 .collect(Collectors.toSet());
555     }
556 
557     /**
558      * Attempts to re-enable BSSIDs that likely experienced failures due to low RSSI.
559      * @param scanDetails
560      */
tryEnablingBlockedBssids(List<ScanDetail> scanDetails)561     public void tryEnablingBlockedBssids(List<ScanDetail> scanDetails) {
562         if (scanDetails == null) {
563             return;
564         }
565         for (ScanDetail scanDetail : scanDetails) {
566             ScanResult scanResult = scanDetail.getScanResult();
567             if (scanResult == null) {
568                 continue;
569             }
570             BssidStatus status = mBssidStatusMap.get(scanResult.BSSID);
571             if (status == null || !status.isInBlocklist
572                     || !LOW_RSSI_SENSITIVE_FAILURES.contains(status.blockReason)) {
573                 continue;
574             }
575             int sufficientRssi = mScoringParams.getSufficientRssi(scanResult.frequency);
576             if (status.lastRssi < sufficientRssi && scanResult.level >= sufficientRssi
577                     && scanResult.level - status.lastRssi >= MIN_RSSI_DIFF_TO_UNBLOCK_BSSID) {
578                 mBssidStatusHistoryLogger.add(status, "rssi significantly improved");
579                 mBssidStatusMap.remove(status.bssid);
580             }
581         }
582     }
583 
584     /**
585      * Removes expired BssidStatus entries and then return remaining entries in the blocklist.
586      * @return Stream of BssidStatus for BSSIDs that are in the blocklist.
587      */
updateAndGetBssidBlocklistInternal()588     private Stream<BssidStatus> updateAndGetBssidBlocklistInternal() {
589         Stream.Builder<BssidStatus> builder = Stream.builder();
590         long curTime = mClock.getWallClockMillis();
591         mBssidStatusMap.entrySet().removeIf(e -> {
592             BssidStatus status = e.getValue();
593             if (status.isInBlocklist) {
594                 if (status.blocklistEndTimeMs < curTime) {
595                     mBssidStatusHistoryLogger.add(status, "updateAndGetBssidBlocklistInternal");
596                     return true;
597                 }
598                 builder.accept(status);
599             }
600             return false;
601         });
602         return builder.build();
603     }
604 
605     /**
606      * Sends the BSSIDs belonging to the input SSID down to the firmware to prevent auto-roaming
607      * to those BSSIDs.
608      * @param ssid
609      */
updateFirmwareRoamingConfiguration(@onNull String ssid)610     public void updateFirmwareRoamingConfiguration(@NonNull String ssid) {
611         if (!mConnectivityHelper.isFirmwareRoamingSupported()) {
612             return;
613         }
614         ArrayList<String> bssidBlocklist = updateAndGetBssidBlocklistInternal()
615                 .filter(entry -> ssid.equals(entry.ssid))
616                 .sorted((o1, o2) -> (int) (o2.blocklistEndTimeMs - o1.blocklistEndTimeMs))
617                 .map(entry -> entry.bssid)
618                 .collect(Collectors.toCollection(ArrayList::new));
619         int fwMaxBlocklistSize = mConnectivityHelper.getMaxNumBlacklistBssid();
620         if (fwMaxBlocklistSize <= 0) {
621             Log.e(TAG, "Invalid max BSSID blocklist size:  " + fwMaxBlocklistSize);
622             return;
623         }
624         // Having the blocklist size exceeding firmware max limit is unlikely because we have
625         // already flitered based on SSID. But just in case this happens, we are prioritizing
626         // sending down BSSIDs blocked for the longest time.
627         if (bssidBlocklist.size() > fwMaxBlocklistSize) {
628             bssidBlocklist = new ArrayList<String>(bssidBlocklist.subList(0,
629                     fwMaxBlocklistSize));
630         }
631         // plumb down to HAL
632         if (!mConnectivityHelper.setFirmwareRoamingConfiguration(bssidBlocklist,
633                 new ArrayList<String>())) {  // TODO(b/36488259): SSID whitelist management.
634         }
635     }
636 
637     @VisibleForTesting
getBssidStatusHistoryLoggerSize()638     public int getBssidStatusHistoryLoggerSize() {
639         return mBssidStatusHistoryLogger.size();
640     }
641 
642     private class BssidStatusHistoryLogger {
643         private LinkedList<String> mLogHistory = new LinkedList<>();
644         private int mBufferSize;
645 
BssidStatusHistoryLogger(int bufferSize)646         BssidStatusHistoryLogger(int bufferSize) {
647             mBufferSize = bufferSize;
648         }
649 
add(BssidStatus bssidStatus, String trigger)650         public void add(BssidStatus bssidStatus, String trigger) {
651             // only log history for Bssids that had been blocked.
652             if (bssidStatus == null || !bssidStatus.isInBlocklist) {
653                 return;
654             }
655             StringBuilder sb = new StringBuilder();
656             mCalendar.setTimeInMillis(mClock.getWallClockMillis());
657             sb.append(", logTimeMs="
658                     + String.format("%tm-%td %tH:%tM:%tS.%tL", mCalendar, mCalendar,
659                     mCalendar, mCalendar, mCalendar, mCalendar));
660             sb.append(", trigger=" + trigger);
661             mLogHistory.add(bssidStatus.toString() + sb.toString());
662             if (mLogHistory.size() > mBufferSize) {
663                 mLogHistory.removeFirst();
664             }
665         }
666 
667         @VisibleForTesting
size()668         public int size() {
669             return mLogHistory.size();
670         }
671 
dump(PrintWriter pw)672         public void dump(PrintWriter pw) {
673             pw.println("BssidBlocklistMonitor - Bssid blocklist history begin ----");
674             for (String line : mLogHistory) {
675                 pw.println(line);
676             }
677             pw.println("BssidBlocklistMonitor - Bssid blocklist history end ----");
678         }
679     }
680 
681     /**
682      * Helper class that counts the number of failures per BSSID.
683      */
684     private class BssidStatus {
685         public final String bssid;
686         public final String ssid;
687         public final int[] failureCount = new int[NUMBER_REASON_CODES];
688         public int blockReason = INVALID_REASON; // reason of blocking this BSSID
689         // The latest RSSI that's seen before this BSSID is added to blocklist.
690         public int lastRssi = 0;
691 
692         // The following are used to flag how long this BSSID stays in the blocklist.
693         public boolean isInBlocklist;
694         public long blocklistEndTimeMs;
695         public long blocklistStartTimeMs;
696 
BssidStatus(String bssid, String ssid)697         BssidStatus(String bssid, String ssid) {
698             this.bssid = bssid;
699             this.ssid = ssid;
700         }
701 
702         /**
703          * increments the failure count for the reasonCode by 1.
704          * @return the incremented failure count
705          */
incrementFailureCount(int reasonCode)706         public int incrementFailureCount(int reasonCode) {
707             return ++failureCount[reasonCode];
708         }
709 
710         /**
711          * Set this BSSID as blocked for the specified duration.
712          * @param durationMs
713          * @param blockReason
714          * @param rssi
715          */
setAsBlocked(long durationMs, @FailureReason int blockReason, int rssi)716         public void setAsBlocked(long durationMs, @FailureReason int blockReason, int rssi) {
717             isInBlocklist = true;
718             blocklistStartTimeMs = mClock.getWallClockMillis();
719             blocklistEndTimeMs = blocklistStartTimeMs + durationMs;
720             this.blockReason = blockReason;
721             lastRssi = rssi;
722         }
723 
724         @Override
toString()725         public String toString() {
726             StringBuilder sb = new StringBuilder();
727             sb.append("BSSID=" + bssid);
728             sb.append(", SSID=" + ssid);
729             sb.append(", isInBlocklist=" + isInBlocklist);
730             if (isInBlocklist) {
731                 sb.append(", blockReason=" + getFailureReasonString(blockReason));
732                 sb.append(", lastRssi=" + lastRssi);
733                 mCalendar.setTimeInMillis(blocklistStartTimeMs);
734                 sb.append(", blocklistStartTimeMs="
735                         + String.format("%tm-%td %tH:%tM:%tS.%tL", mCalendar, mCalendar,
736                         mCalendar, mCalendar, mCalendar, mCalendar));
737                 mCalendar.setTimeInMillis(blocklistEndTimeMs);
738                 sb.append(", blocklistEndTimeMs="
739                         + String.format("%tm-%td %tH:%tM:%tS.%tL", mCalendar, mCalendar,
740                         mCalendar, mCalendar, mCalendar, mCalendar));
741             }
742             return sb.toString();
743         }
744     }
745 }
746