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