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