Add BssidBlocklistMonitor class

Moving logic related to blocklisting bssids into its own class.
This first version extends existing bssid failure tracking from just
association failures to all connection failures reported by connection
events.

Bug: 139287182
Test: unit tests
Change-Id: Ieac59949459db7ea3b7d90a47d55affa9a25c245
diff --git a/service/java/com/android/server/wifi/BssidBlocklistMonitor.java b/service/java/com/android/server/wifi/BssidBlocklistMonitor.java
new file mode 100644
index 0000000..9a88bdf
--- /dev/null
+++ b/service/java/com/android/server/wifi/BssidBlocklistMonitor.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wifi;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.net.wifi.WifiSsid;
+import android.util.ArrayMap;
+import android.util.LocalLog;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * This classes manages the addition and removal of BSSIDs to the BSSID blocklist, which is used
+ * for firmware roaming and network selection.
+ */
+public class BssidBlocklistMonitor {
+    // A special type association rejection
+    public static final int REASON_AP_UNABLE_TO_HANDLE_NEW_STA = 0;
+    // No internet
+    public static final int REASON_NETWORK_VALIDATION_FAILURE = 1;
+    // Wrong password error
+    public static final int REASON_WRONG_PASSWORD = 2;
+    // Incorrect EAP credentials
+    public static final int REASON_EAP_FAILURE = 3;
+    // Other association rejection failures
+    public static final int REASON_ASSOCIATION_REJECTION = 4;
+    // Associated timeout failures, when the RSSI is good
+    public static final int REASON_ASSOCIATION_TIMEOUT = 5;
+    // Other authentication failures
+    public static final int REASON_AUTHENTICATION_FAILURE = 6;
+    // DHCP failures
+    public static final int REASON_DHCP_FAILURE = 7;
+    // Local constant being used to keep track of how many failure reasons there are.
+    private static final int NUMBER_REASON_CODES = 8;
+
+    @IntDef(prefix = { "REASON_" }, value = {
+            REASON_AP_UNABLE_TO_HANDLE_NEW_STA,
+            REASON_NETWORK_VALIDATION_FAILURE,
+            REASON_WRONG_PASSWORD,
+            REASON_EAP_FAILURE,
+            REASON_ASSOCIATION_REJECTION,
+            REASON_ASSOCIATION_TIMEOUT,
+            REASON_AUTHENTICATION_FAILURE,
+            REASON_DHCP_FAILURE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BssidBlocklistMonitorFailureReason {}
+
+    public static final int[] FAILURE_COUNT_DISABLE_THRESHOLD = {
+            1,  //  threshold for REASON_AP_UNABLE_TO_HANDLE_NEW_STA
+            1,  //  threshold for REASON_NETWORK_VALIDATION_FAILURE
+            1,  //  threshold for REASON_WRONG_PASSWORD
+            1,  //  threshold for REASON_EAP_FAILURE
+            3,  //  threshold for REASON_ASSOCIATION_REJECTION
+            3,  //  threshold for REASON_ASSOCIATION_TIMEOUT
+            3,  //  threshold for REASON_AUTHENTICATION_FAILURE
+            3   //  threshold for REASON_DHCP_FAILURE
+    };
+
+    private static final int FAILURE_COUNTER_THRESHOLD = 3;
+    private static final long BASE_BLOCKLIST_DURATION = 5 * 60 * 1000; // 5 minutes
+    private static final String TAG = "BssidBlocklistMonitor";
+
+    private final WifiLastResortWatchdog mWifiLastResortWatchdog;
+    private final WifiConnectivityHelper mConnectivityHelper;
+    private final Clock mClock;
+    private final LocalLog mLocalLog;
+    private final Calendar mCalendar;
+
+    // Map of bssid to BssidStatus
+    private Map<String, BssidStatus> mBssidStatusMap = new ArrayMap<>();
+
+    /**
+     * Create a new instance of BssidBlocklistMonitor
+     */
+    BssidBlocklistMonitor(WifiConnectivityHelper connectivityHelper,
+            WifiLastResortWatchdog wifiLastResortWatchdog, Clock clock, LocalLog localLog) {
+        mConnectivityHelper = connectivityHelper;
+        mWifiLastResortWatchdog = wifiLastResortWatchdog;
+        mClock = clock;
+        mLocalLog = localLog;
+        mCalendar = Calendar.getInstance();
+    }
+
+    // A helper to log debugging information in the local log buffer, which can
+    // be retrieved in bugreport.
+    private void localLog(String log) {
+        mLocalLog.log(log);
+    }
+
+    private long getBlocklistDurationWithExponentialBackoff(String bssid) {
+        // TODO: b/139287182 implement exponential backoff to extend the blocklist duration for
+        // BSSIDs that continue to fail.
+        return BASE_BLOCKLIST_DURATION;
+    }
+
+    /**
+     * Dump the local log buffer and other internal state of BssidBlocklistMonitor.
+     */
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("Dump of BssidBlocklistMonitor");
+        pw.println("BssidBlocklistMonitor - Bssid blocklist Begin ----");
+        mBssidStatusMap.values().stream().forEach(entry -> pw.println(entry));
+        pw.println("BssidBlocklistMonitor - Bssid blocklist End ----");
+    }
+
+    private boolean addToBlocklist(@NonNull BssidStatus entry) {
+        // Call mWifiLastResortWatchdog.shouldIgnoreBssidUpdate to give watchdog a chance to
+        // trigger before blocklisting the bssid.
+        String bssid = entry.bssid;
+        if (mWifiLastResortWatchdog.shouldIgnoreBssidUpdate(bssid)) {
+            return false;
+        }
+        long durationMs = getBlocklistDurationWithExponentialBackoff(bssid);
+        entry.addToBlocklist(durationMs);
+        localLog(TAG + " addToBlocklist: bssid=" + bssid + ", ssid=" + entry.ssid
+                + ", durationMs=" + durationMs);
+        return true;
+    }
+
+    /**
+     * increments the number of failures for the given bssid and returns the number of failures so
+     * far.
+     * @return the BssidStatus for the BSSID
+     */
+    private @NonNull BssidStatus incrementFailureCountForBssid(
+            @NonNull String bssid, @NonNull String ssid, int reasonCode) {
+        BssidStatus status = mBssidStatusMap.get(bssid);
+        if (status == null || !ssid.equals(status.ssid)) {
+            if (status != null) {
+                localLog("incrementFailureCountForBssid: BSSID=" + bssid + ", SSID changed from "
+                        + status.ssid + " to " + ssid);
+            }
+            status = new BssidStatus(bssid, ssid);
+            mBssidStatusMap.put(bssid, status);
+        }
+        status.incrementFailureCount(reasonCode);
+        return status;
+    }
+
+    /**
+     * Note a failure event on a bssid and perform appropriate actions.
+     * @return True if the blocklist has been modified.
+     */
+    public boolean handleBssidConnectionFailure(String bssid, String ssid,
+            @BssidBlocklistMonitorFailureReason int reasonCode) {
+        if (bssid == null || ssid == null || WifiSsid.NONE.equals(ssid)
+                || bssid.equals(ClientModeImpl.SUPPLICANT_BSSID_ANY)
+                || reasonCode < 0 || reasonCode >= NUMBER_REASON_CODES) {
+            Log.e(TAG, "Invalid input: BSSID=" + bssid + ", SSID=" + ssid
+                    + ", reasonCode=" + reasonCode);
+            return false;
+        }
+        boolean result = false;
+        BssidStatus entry = incrementFailureCountForBssid(bssid, ssid, reasonCode);
+        if (entry.failureCount[reasonCode] >= FAILURE_COUNT_DISABLE_THRESHOLD[reasonCode]) {
+            result = addToBlocklist(entry);
+        }
+        return result;
+    }
+
+    /**
+     * Note a connection success event on a bssid and clear appropriate failure counters.
+     */
+    public void handleBssidConnectionSuccess(@NonNull String bssid) {
+        BssidStatus status = mBssidStatusMap.get(bssid);
+        if (status == null) {
+            return;
+        }
+        // Clear the L2 failure counters
+        status.failureCount[REASON_AP_UNABLE_TO_HANDLE_NEW_STA] = 0;
+        status.failureCount[REASON_WRONG_PASSWORD] = 0;
+        status.failureCount[REASON_EAP_FAILURE] = 0;
+        status.failureCount[REASON_ASSOCIATION_REJECTION] = 0;
+        status.failureCount[REASON_ASSOCIATION_TIMEOUT] = 0;
+        status.failureCount[REASON_AUTHENTICATION_FAILURE] = 0;
+    }
+
+    /**
+     * Note a successful network validation on a BSSID and clear appropriate failure counters.
+     */
+    public void handleNetworkValidationSuccess(@NonNull String bssid) {
+        BssidStatus status = mBssidStatusMap.get(bssid);
+        if (status == null) {
+            return;
+        }
+        status.failureCount[REASON_NETWORK_VALIDATION_FAILURE] = 0;
+    }
+
+    /**
+     * Note a successful DHCP provisioning and clear appropriate faliure counters.
+     */
+    public void handleDhcpProvisioningSuccess(@NonNull String bssid) {
+        BssidStatus status = mBssidStatusMap.get(bssid);
+        if (status == null) {
+            return;
+        }
+        status.failureCount[REASON_DHCP_FAILURE] = 0;
+    }
+
+    /**
+     * Clears the blocklist for BSSIDs associated with the input SSID only.
+     * @param ssid
+     */
+    public void clearBssidBlocklistForSsid(@NonNull String ssid) {
+        int prevSize = mBssidStatusMap.size();
+        mBssidStatusMap.entrySet().removeIf(e -> e.getValue().ssid.equals(ssid));
+        int diff = prevSize - mBssidStatusMap.size();
+        if (diff > 0) {
+            localLog(TAG + " clearBssidBlocklistForSsid: SSID=" + ssid
+                    + ", num BSSIDs cleared=" + diff);
+        }
+    }
+
+    /**
+     * Clears the BSSID blocklist and failure counters.
+     */
+    public void clearBssidBlocklist() {
+        if (mBssidStatusMap.size() > 0) {
+            localLog(TAG + " clearBssidBlocklist: num BSSIDs cleared=" + mBssidStatusMap.size());
+            mBssidStatusMap.clear();
+        }
+    }
+
+    /**
+     * Gets the BSSIDs that are currently in the blocklist.
+     * @return Set of BSSIDs currently in the blocklist
+     */
+    public Set<String> updateAndGetBssidBlocklist() {
+        return updateAndGetBssidBlocklistInternal()
+                .map(entry -> entry.bssid)
+                .collect(Collectors.toSet());
+    }
+
+    /**
+     * Removes expired BssidStatus entries and then return remaining entries in the blocklist.
+     * @return Stream of BssidStatus for BSSIDs that are in the blocklist.
+     */
+    private Stream<BssidStatus> updateAndGetBssidBlocklistInternal() {
+        Stream.Builder<BssidStatus> builder = Stream.builder();
+        long curTime = mClock.getWallClockMillis();
+        mBssidStatusMap.entrySet().removeIf(e -> {
+            BssidStatus status = e.getValue();
+            if (status.isInBlocklist) {
+                if (status.blocklistEndTimeMs < curTime) {
+                    return true;
+                }
+                builder.accept(status);
+            }
+            return false;
+        });
+        return builder.build();
+    }
+
+    /**
+     * Sends the BSSIDs belonging to the input SSID down to the firmware to prevent auto-roaming
+     * to those BSSIDs.
+     * @param ssid
+     */
+    public void updateFirmwareRoamingConfiguration(@NonNull String ssid) {
+        if (!mConnectivityHelper.isFirmwareRoamingSupported()) {
+            return;
+        }
+        ArrayList<String> bssidBlocklist = updateAndGetBssidBlocklistInternal()
+                .filter(entry -> ssid.equals(entry.ssid))
+                .sorted((o1, o2) -> (int) (o2.blocklistEndTimeMs - o1.blocklistEndTimeMs))
+                .map(entry -> entry.bssid)
+                .collect(Collectors.toCollection(ArrayList::new));
+        int fwMaxBlocklistSize = mConnectivityHelper.getMaxNumBlacklistBssid();
+        if (fwMaxBlocklistSize <= 0) {
+            Log.e(TAG, "Invalid max BSSID blocklist size:  " + fwMaxBlocklistSize);
+            return;
+        }
+        // Having the blocklist size exceeding firmware max limit is unlikely because we have
+        // already flitered based on SSID. But just in case this happens, we are prioritizing
+        // sending down BSSIDs blocked for the longest time.
+        if (bssidBlocklist.size() > fwMaxBlocklistSize) {
+            bssidBlocklist = new ArrayList<String>(bssidBlocklist.subList(0,
+                    fwMaxBlocklistSize));
+        }
+        // plumb down to HAL
+        if (!mConnectivityHelper.setFirmwareRoamingConfiguration(bssidBlocklist,
+                new ArrayList<String>())) {  // TODO(b/36488259): SSID whitelist management.
+        }
+    }
+
+    /**
+     * Helper class that counts the number of failures per BSSID.
+     */
+    private class BssidStatus {
+        public final String bssid;
+        public final String ssid;
+        public final int[] failureCount = new int[NUMBER_REASON_CODES];
+
+        // The following are used to flag how long this BSSID stays in the blocklist.
+        public boolean isInBlocklist;
+        public long blocklistEndTimeMs;
+
+
+        BssidStatus(String bssid, String ssid) {
+            this.bssid = bssid;
+            this.ssid = ssid;
+        }
+
+        /**
+         * increments the failure count for the reasonCode by 1.
+         * @return the incremented failure count
+         */
+        public int incrementFailureCount(int reasonCode) {
+            return ++failureCount[reasonCode];
+        }
+
+        /**
+         * Add this BSSID to blocklist for the specified duration.
+         * @param durationMs
+         */
+        public void addToBlocklist(long durationMs) {
+            isInBlocklist = true;
+            blocklistEndTimeMs = mClock.getWallClockMillis() + durationMs;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("BSSID=" + bssid);
+            sb.append(", SSID=" + ssid);
+            sb.append(", isInBlocklist=" + isInBlocklist);
+            if (isInBlocklist) {
+                mCalendar.setTimeInMillis(blocklistEndTimeMs);
+                sb.append(", blocklistEndTimeMs="
+                        + String.format("%tm-%td %tH:%tM:%tS.%tL", mCalendar, mCalendar,
+                        mCalendar, mCalendar, mCalendar, mCalendar));
+            }
+            return sb.toString();
+        }
+    }
+}
diff --git a/service/java/com/android/server/wifi/WifiInjector.java b/service/java/com/android/server/wifi/WifiInjector.java
index ef7d5fe..b345a81 100644
--- a/service/java/com/android/server/wifi/WifiInjector.java
+++ b/service/java/com/android/server/wifi/WifiInjector.java
@@ -151,6 +151,7 @@
     private final LinkProbeManager mLinkProbeManager;
     private IpMemoryStore mIpMemoryStore;
     private final WifiThreadRunner mWifiThreadRunner;
+    private BssidBlocklistMonitor mBssidBlocklistMonitor;
 
     public WifiInjector(Context context) {
         if (context == null) {
@@ -579,6 +580,8 @@
         mWifiLastResortWatchdog = new WifiLastResortWatchdog(this, mContext, mClock,
                 mWifiMetrics, clientModeImpl, mWifiHandlerThread.getLooper(), mDeviceConfigFacade,
                 mWifiThreadRunner);
+        mBssidBlocklistMonitor = new BssidBlocklistMonitor(mWifiConnectivityHelper,
+                mWifiLastResortWatchdog, mClock, mConnectivityLocalLog);
         return new WifiConnectivityManager(mContext, getScoringParams(),
                 clientModeImpl, this,
                 mWifiConfigManager, clientModeImpl.getWifiInfo(),
@@ -721,6 +724,10 @@
         return mIpMemoryStore;
     }
 
+    public BssidBlocklistMonitor getBssidBlocklistMonitor() {
+        return mBssidBlocklistMonitor;
+    }
+
     public HostapdHal getHostapdHal() {
         return mHostapdHal;
     }
diff --git a/tests/wifitests/src/com/android/server/wifi/BssidBlocklistMonitorTest.java b/tests/wifitests/src/com/android/server/wifi/BssidBlocklistMonitorTest.java
new file mode 100644
index 0000000..985dd45
--- /dev/null
+++ b/tests/wifitests/src/com/android/server/wifi/BssidBlocklistMonitorTest.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wifi;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import android.util.LocalLog;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+/**
+ * Unit tests for {@link com.android.server.wifi.BssidBlocklistMonitor}.
+ */
+@SmallTest
+public class BssidBlocklistMonitorTest {
+    private static final int TEST_NUM_MAX_FIRMWARE_SUPPORT_BSSIDS = 3;
+    private static final String TEST_SSID_1 = "TestSSID1";
+    private static final String TEST_SSID_2 = "TestSSID2";
+    private static final String TEST_SSID_3 = "TestSSID3";
+    private static final String TEST_BSSID_1 = "0a:08:5c:67:89:00";
+    private static final String TEST_BSSID_2 = "0a:08:5c:67:89:01";
+    private static final String TEST_BSSID_3 = "0a:08:5c:67:89:02";
+    private static final int TEST_L2_FAILURE = BssidBlocklistMonitor.REASON_ASSOCIATION_REJECTION;
+    private static final int TEST_DHCP_FAILURE = BssidBlocklistMonitor.REASON_DHCP_FAILURE;
+    private static final long BASE_BLOCKLIST_DURATION = 5 * 60 * 1000; // 5 minutes
+    private static final int NUM_FAILURES_TO_BLOCKLIST = 3;
+
+    @Mock private WifiConnectivityHelper mWifiConnectivityHelper;
+    @Mock private WifiLastResortWatchdog mWifiLastResortWatchdog;
+    @Mock private Clock mClock;
+    @Mock private LocalLog mLocalLog;
+
+    private BssidBlocklistMonitor mBssidBlocklistMonitor;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mWifiConnectivityHelper.isFirmwareRoamingSupported()).thenReturn(true);
+        when(mWifiConnectivityHelper.getMaxNumBlacklistBssid())
+                .thenReturn(TEST_NUM_MAX_FIRMWARE_SUPPORT_BSSIDS);
+        mBssidBlocklistMonitor = new BssidBlocklistMonitor(mWifiConnectivityHelper,
+                mWifiLastResortWatchdog, mClock, mLocalLog);
+    }
+
+    private void verifyAddTestBssidToBlocklist() {
+        mBssidBlocklistMonitor.handleBssidConnectionFailure(
+                TEST_BSSID_1, TEST_SSID_1,
+                BssidBlocklistMonitor.REASON_AP_UNABLE_TO_HANDLE_NEW_STA);
+        assertTrue(mBssidBlocklistMonitor.updateAndGetBssidBlocklist().contains(TEST_BSSID_1));
+    }
+
+    // Verify adding 2 BSSID for SSID_1 and 1 BSSID for SSID_2 to the blocklist.
+    private void verifyAddMultipleBssidsToBlocklist() {
+        when(mClock.getWallClockMillis()).thenReturn(0L);
+        mBssidBlocklistMonitor.handleBssidConnectionFailure(TEST_BSSID_1,
+                TEST_SSID_1, BssidBlocklistMonitor.REASON_AP_UNABLE_TO_HANDLE_NEW_STA);
+        when(mClock.getWallClockMillis()).thenReturn(1L);
+        mBssidBlocklistMonitor.handleBssidConnectionFailure(TEST_BSSID_2,
+                TEST_SSID_1, BssidBlocklistMonitor.REASON_AP_UNABLE_TO_HANDLE_NEW_STA);
+        mBssidBlocklistMonitor.handleBssidConnectionFailure(TEST_BSSID_3,
+                TEST_SSID_2, BssidBlocklistMonitor.REASON_AP_UNABLE_TO_HANDLE_NEW_STA);
+
+        // Verify that we have 3 BSSIDs in the blocklist.
+        Set<String> bssidList = mBssidBlocklistMonitor.updateAndGetBssidBlocklist();
+        assertEquals(3, bssidList.size());
+        assertTrue(bssidList.contains(TEST_BSSID_1));
+        assertTrue(bssidList.contains(TEST_BSSID_2));
+        assertTrue(bssidList.contains(TEST_BSSID_3));
+    }
+
+    private void handleBssidConnectionFailureMultipleTimes(String bssid, int reason, int times) {
+        handleBssidConnectionFailureMultipleTimes(bssid, TEST_SSID_1, reason, times);
+    }
+
+    private void handleBssidConnectionFailureMultipleTimes(String bssid, String ssid, int reason,
+            int times) {
+        for (int i = 0; i < times; i++) {
+            mBssidBlocklistMonitor.handleBssidConnectionFailure(bssid, ssid, reason);
+        }
+    }
+
+    /**
+     * Verify that updateAndGetBssidBlocklist removes expired blocklist entries and clears
+     * all failure counters for those networks.
+     */
+    @Test
+    public void testBssidIsRemovedFromBlocklistAfterTimeout() {
+        verifyAddTestBssidToBlocklist();
+        // Verify TEST_BSSID_1 is not removed from the blocklist until sufficient time have passed.
+        when(mClock.getWallClockMillis()).thenReturn(BASE_BLOCKLIST_DURATION);
+        assertTrue(mBssidBlocklistMonitor.updateAndGetBssidBlocklist().contains(TEST_BSSID_1));
+
+        // Verify that TEST_BSSID_1 is removed from the blocklist after the timeout duration.
+        when(mClock.getWallClockMillis()).thenReturn(BASE_BLOCKLIST_DURATION + 1);
+        assertEquals(0, mBssidBlocklistMonitor.updateAndGetBssidBlocklist().size());
+    }
+
+    /**
+     * Verify that consecutive failures will add a BSSID to blocklist.
+     */
+    @Test
+    public void testRepeatedConnectionFailuresAddToBlocklist() {
+        // First verify that n-1 failrues does not add the BSSID to blocklist
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_L2_FAILURE,
+                NUM_FAILURES_TO_BLOCKLIST - 1);
+        assertEquals(0, mBssidBlocklistMonitor.updateAndGetBssidBlocklist().size());
+
+        // Simulate a long time passing to make sure failure counters are not being cleared through
+        // some time based check
+        when(mClock.getWallClockMillis()).thenReturn(10 * BASE_BLOCKLIST_DURATION);
+        assertEquals(0, mBssidBlocklistMonitor.updateAndGetBssidBlocklist().size());
+
+        // Verify that 1 more failure will add the BSSID to blacklist.
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_L2_FAILURE, 1);
+        assertTrue(mBssidBlocklistMonitor.updateAndGetBssidBlocklist().contains(TEST_BSSID_1));
+    }
+
+    /**
+     * Verify that onSuccessfulConnection resets L2 related failure counts.
+     */
+    @Test
+    public void testL2FailureCountIsResetAfterSuccessfulConnection() {
+        // First simulate getting a particular L2 failure n-1 times
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_L2_FAILURE,
+                NUM_FAILURES_TO_BLOCKLIST - 1);
+
+        // Verify that a connection success event will clear the failure count.
+        mBssidBlocklistMonitor.handleBssidConnectionSuccess(TEST_BSSID_1);
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_L2_FAILURE,
+                NUM_FAILURES_TO_BLOCKLIST - 1);
+
+        // Verify we have not blocklisted anything yet because the failure count was cleared.
+        assertEquals(0, mBssidBlocklistMonitor.updateAndGetBssidBlocklist().size());
+
+        // Verify that TEST_BSSID_1 is added to blocklist after 1 more failure.
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_L2_FAILURE, 1);
+        assertTrue(mBssidBlocklistMonitor.updateAndGetBssidBlocklist().contains(TEST_BSSID_1));
+    }
+
+    /**
+     * Verify that handleDhcpProvisioningSuccess resets DHCP failure counts.
+     */
+    @Test
+    public void testL3FailureCountIsResetAfterDhcpConfiguration() {
+        // First simulate getting an DHCP failure n-1 times.
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_DHCP_FAILURE,
+                NUM_FAILURES_TO_BLOCKLIST - 1);
+
+        // Verify that a network validation success event will clear the failure count.
+        mBssidBlocklistMonitor.handleDhcpProvisioningSuccess(TEST_BSSID_1);
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_DHCP_FAILURE,
+                NUM_FAILURES_TO_BLOCKLIST - 1);
+
+        // Verify we have not blocklisted anything yet because the failure count was cleared.
+        assertEquals(0, mBssidBlocklistMonitor.updateAndGetBssidBlocklist().size());
+
+        // Verify that TEST_BSSID_1 is added to blocklist after 1 more failure.
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_DHCP_FAILURE, 1);
+        assertTrue(mBssidBlocklistMonitor.updateAndGetBssidBlocklist().contains(TEST_BSSID_1));
+    }
+
+    /**
+     * Verify that L3 failure counts are not affected when L2 failure counts are reset.
+     */
+    @Test
+    public void testL3FailureCountIsNotResetByConnectionSuccess() {
+        // First simulate getting an L3 failure n-1 times.
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_DHCP_FAILURE,
+                NUM_FAILURES_TO_BLOCKLIST - 1);
+        assertEquals(0, mBssidBlocklistMonitor.updateAndGetBssidBlocklist().size());
+
+        // Verify that the failure counter is not cleared by |handleBssidConnectionSuccess|.
+        mBssidBlocklistMonitor.handleBssidConnectionSuccess(TEST_BSSID_1);
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_DHCP_FAILURE, 1);
+        assertEquals(1, mBssidBlocklistMonitor.updateAndGetBssidBlocklist().size());
+    }
+
+    /**
+     * Verify that when a failure signal is received for a BSSID with different SSID from before,
+     * then the failure counts are reset.
+     */
+    @Test
+    public void testFailureCountIsResetIfSsidChanges() {
+        // First simulate getting a particular L2 failure n-1 times
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_L2_FAILURE,
+                NUM_FAILURES_TO_BLOCKLIST - 1);
+
+        // Verify that when the SSID changes, the failure counts are cleared.
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_SSID_2, TEST_L2_FAILURE,
+                NUM_FAILURES_TO_BLOCKLIST - 1);
+
+        // Verify we have not blocklisted anything yet because the failure count was cleared.
+        assertEquals(0, mBssidBlocklistMonitor.updateAndGetBssidBlocklist().size());
+
+        // Verify that TEST_BSSID_1 is added to blocklist after 1 more failure.
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_SSID_2, TEST_L2_FAILURE, 1);
+        assertTrue(mBssidBlocklistMonitor.updateAndGetBssidBlocklist().contains(TEST_BSSID_1));
+    }
+
+    /**
+     * Verify that a BSSID is not added to blocklist as long as
+     * mWifiLastResortWatchdog.shouldIgnoreBssidUpdate is returning true.
+     */
+    @Test
+    public void testWatchdogIsGivenChanceToTrigger() {
+        // Verify that |shouldIgnoreBssidUpdate| can prevent a BSSID from being added to blocklist.
+        when(mWifiLastResortWatchdog.shouldIgnoreBssidUpdate(anyString())).thenReturn(true);
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_L2_FAILURE,
+                NUM_FAILURES_TO_BLOCKLIST * 2);
+        assertEquals(0, mBssidBlocklistMonitor.updateAndGetBssidBlocklist().size());
+
+        // Verify that after watchdog is okay with blocking a BSSID, it gets blocked after 1
+        // more failure.
+        when(mWifiLastResortWatchdog.shouldIgnoreBssidUpdate(anyString())).thenReturn(false);
+        handleBssidConnectionFailureMultipleTimes(TEST_BSSID_1, TEST_L2_FAILURE, 1);
+        assertTrue(mBssidBlocklistMonitor.updateAndGetBssidBlocklist().contains(TEST_BSSID_1));
+    }
+
+    /**
+     * Verify that we are correctly filtering by SSID when sending a blocklist down to firmware.
+     */
+    @Test
+    public void testSendBlocklistToFirmwareFilterBySsid() {
+        verifyAddMultipleBssidsToBlocklist();
+
+        // Verify we are sending 2 BSSIDs down to the firmware for SSID_1.
+        ArrayList<String> blocklist1 = new ArrayList<>();
+        blocklist1.add(TEST_BSSID_2);
+        blocklist1.add(TEST_BSSID_1);
+        mBssidBlocklistMonitor.updateFirmwareRoamingConfiguration(TEST_SSID_1);
+        verify(mWifiConnectivityHelper).setFirmwareRoamingConfiguration(eq(blocklist1),
+                eq(new ArrayList<>()));
+
+        // Verify we are sending 1 BSSID down to the firmware for SSID_2.
+        ArrayList<String> blocklist2 = new ArrayList<>();
+        blocklist2.add(TEST_BSSID_3);
+        mBssidBlocklistMonitor.updateFirmwareRoamingConfiguration(TEST_SSID_2);
+        verify(mWifiConnectivityHelper).setFirmwareRoamingConfiguration(eq(blocklist2),
+                eq(new ArrayList<>()));
+
+        // Verify we are not sending any BSSIDs down to the firmware since there does not
+        // exists any BSSIDs for TEST_SSID_3 in the blocklist.
+        mBssidBlocklistMonitor.updateFirmwareRoamingConfiguration(TEST_SSID_3);
+        verify(mWifiConnectivityHelper).setFirmwareRoamingConfiguration(eq(new ArrayList<>()),
+                eq(new ArrayList<>()));
+    }
+
+    /**
+     * Verify that when sending the blocklist down to firmware, the list is sorted by latest
+     * end time first.
+     * Also verify that when there are more blocklisted BSSIDs than the allowed limit by the
+     * firmware, the list sent down is trimmed.
+     */
+    @Test
+    public void testMostRecentBlocklistEntriesAreSentToFirmware() {
+        // Add BSSIDs to blocklist
+        String bssid = "0a:08:5c:67:89:0";
+        for (int i = 0; i < 10; i++) {
+            when(mClock.getWallClockMillis()).thenReturn((long) i);
+            mBssidBlocklistMonitor.handleBssidConnectionFailure(bssid + i,
+                    TEST_SSID_1, BssidBlocklistMonitor.REASON_AP_UNABLE_TO_HANDLE_NEW_STA);
+
+            // This will build a List of BSSIDs starting from the latest added ones that is at
+            // most size |TEST_NUM_MAX_FIRMWARE_SUPPORT_BSSIDS|.
+            // Then verify that the blocklist is sent down in this sorted order.
+            ArrayList<String> blocklist = new ArrayList<>();
+            for (int j = i; j > i - TEST_NUM_MAX_FIRMWARE_SUPPORT_BSSIDS; j--) {
+                if (j < 0) {
+                    break;
+                }
+                blocklist.add(bssid + j);
+            }
+            mBssidBlocklistMonitor.updateFirmwareRoamingConfiguration(TEST_SSID_1);
+            verify(mWifiConnectivityHelper).setFirmwareRoamingConfiguration(eq(blocklist),
+                    eq(new ArrayList<>()));
+        }
+        assertEquals(10, mBssidBlocklistMonitor.updateAndGetBssidBlocklist().size());
+    }
+
+    /**
+     * Verifies that when firmware roaming is disabled, the blocklist does not get plumbed to
+     * hardware, but the blocklist should still accessible by the framework.
+     */
+    @Test
+    public void testFirmwareRoamingDisabled() {
+        when(mWifiConnectivityHelper.isFirmwareRoamingSupported()).thenReturn(false);
+        verifyAddTestBssidToBlocklist();
+
+        mBssidBlocklistMonitor.updateFirmwareRoamingConfiguration(TEST_SSID_1);
+        verify(mWifiConnectivityHelper, never()).setFirmwareRoamingConfiguration(any(), any());
+    }
+
+    /**
+     * Verify that clearBssidBlocklist resets internal state.
+     */
+    @Test
+    public void testClearBssidBlocklist() {
+        verifyAddTestBssidToBlocklist();
+        mBssidBlocklistMonitor.clearBssidBlocklist();
+        assertEquals(0, mBssidBlocklistMonitor.updateAndGetBssidBlocklist().size());
+    }
+
+    /**
+     * Verify that the BSSID blocklist is cleared for the entire network.
+     */
+    @Test
+    public void testClearBssidBlocklistForSsid() {
+        verifyAddMultipleBssidsToBlocklist();
+
+        // Clear the blocklist for SSID 1.
+        mBssidBlocklistMonitor.clearBssidBlocklistForSsid(TEST_SSID_1);
+
+        // Verify that the blocklist is deleted for SSID 1 and the BSSID for SSID 2 still remains.
+        Set<String> bssidList = mBssidBlocklistMonitor.updateAndGetBssidBlocklist();
+        assertEquals(1, bssidList.size());
+        assertTrue(bssidList.contains(TEST_BSSID_3));
+    }
+}