• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.wifi;
18 
19 import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.DISABLE_REASON_INFOS;
20 
21 import android.annotation.IntDef;
22 import android.annotation.NonNull;
23 import android.content.Context;
24 import android.net.wifi.ScanResult;
25 import android.net.wifi.WifiConfiguration;
26 import android.net.wifi.WifiConfiguration.NetworkSelectionStatus;
27 import android.net.wifi.WifiConfiguration.NetworkSelectionStatus.DisableReasonInfo;
28 import android.net.wifi.WifiConfiguration.NetworkSelectionStatus.NetworkSelectionDisableReason;
29 import android.net.wifi.WifiSsid;
30 import android.util.ArrayMap;
31 import android.util.ArraySet;
32 import android.util.LocalLog;
33 import android.util.Log;
34 import android.util.SparseArray;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.server.wifi.util.WifiPermissionsUtil;
38 import com.android.wifi.resources.R;
39 
40 import java.io.FileDescriptor;
41 import java.io.PrintWriter;
42 import java.lang.annotation.Retention;
43 import java.lang.annotation.RetentionPolicy;
44 import java.util.ArrayList;
45 import java.util.Calendar;
46 import java.util.Collections;
47 import java.util.HashSet;
48 import java.util.LinkedList;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Set;
52 import java.util.concurrent.TimeUnit;
53 import java.util.stream.Collectors;
54 import java.util.stream.Stream;
55 
56 /**
57  * This class manages the addition and removal of BSSIDs to the BSSID blocklist, which is used
58  * for firmware roaming and network selection.
59  */
60 public class WifiBlocklistMonitor {
61     // A special type association rejection
62     public static final int REASON_AP_UNABLE_TO_HANDLE_NEW_STA = 0;
63     // No internet
64     public static final int REASON_NETWORK_VALIDATION_FAILURE = 1;
65     // Wrong password error
66     public static final int REASON_WRONG_PASSWORD = 2;
67     // Incorrect EAP credentials
68     public static final int REASON_EAP_FAILURE = 3;
69     // Other association rejection failures
70     public static final int REASON_ASSOCIATION_REJECTION = 4;
71     // Association timeout failures.
72     public static final int REASON_ASSOCIATION_TIMEOUT = 5;
73     // Other authentication failures
74     public static final int REASON_AUTHENTICATION_FAILURE = 6;
75     // DHCP failures
76     public static final int REASON_DHCP_FAILURE = 7;
77     // Abnormal disconnect error
78     public static final int REASON_ABNORMAL_DISCONNECT = 8;
79     // AP initiated disconnect for a given duration.
80     public static final int REASON_FRAMEWORK_DISCONNECT_MBO_OCE = 9;
81     // Avoid connecting to the failed AP when trying to reconnect on other available candidates.
82     public static final int REASON_FRAMEWORK_DISCONNECT_FAST_RECONNECT = 10;
83     // The connected scorer has disconnected this network.
84     public static final int REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE = 11;
85     // Non-local disconnection in the middle of connecting state
86     public static final int REASON_NONLOCAL_DISCONNECT_CONNECTING = 12;
87     // Constant being used to keep track of how many failure reasons there are.
88     public static final int NUMBER_REASON_CODES = 13;
89     public static final int INVALID_REASON = -1;
90 
91     @IntDef(prefix = { "REASON_" }, value = {
92             REASON_AP_UNABLE_TO_HANDLE_NEW_STA,
93             REASON_NETWORK_VALIDATION_FAILURE,
94             REASON_WRONG_PASSWORD,
95             REASON_EAP_FAILURE,
96             REASON_ASSOCIATION_REJECTION,
97             REASON_ASSOCIATION_TIMEOUT,
98             REASON_AUTHENTICATION_FAILURE,
99             REASON_DHCP_FAILURE,
100             REASON_ABNORMAL_DISCONNECT,
101             REASON_FRAMEWORK_DISCONNECT_MBO_OCE,
102             REASON_FRAMEWORK_DISCONNECT_FAST_RECONNECT,
103             REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE,
104             REASON_NONLOCAL_DISCONNECT_CONNECTING
105     })
106     @Retention(RetentionPolicy.SOURCE)
107     public @interface FailureReason {}
108 
109     // To be filled with values from the overlay.
110     private static final int[] FAILURE_COUNT_DISABLE_THRESHOLD = new int[NUMBER_REASON_CODES];
111     private boolean mFailureCountDisableThresholdArrayInitialized = false;
112     private static final long ABNORMAL_DISCONNECT_RESET_TIME_MS = TimeUnit.HOURS.toMillis(3);
113     private static final int MIN_RSSI_DIFF_TO_UNBLOCK_BSSID = 5;
114     @VisibleForTesting
115     public static final int NUM_CONSECUTIVE_FAILURES_PER_NETWORK_EXP_BACKOFF = 5;
116     @VisibleForTesting
117     public static final long WIFI_CONFIG_MAX_DISABLE_DURATION_MILLIS = TimeUnit.HOURS.toMillis(18);
118     private static final String TAG = "WifiBlocklistMonitor";
119 
120     private final Context mContext;
121     private final WifiLastResortWatchdog mWifiLastResortWatchdog;
122     private final WifiConnectivityHelper mConnectivityHelper;
123     private final Clock mClock;
124     private final LocalLog mLocalLog;
125     private final WifiScoreCard mWifiScoreCard;
126     private final ScoringParams mScoringParams;
127     private final WifiMetrics mWifiMetrics;
128     private final WifiPermissionsUtil mWifiPermissionsUtil;
129     private final Map<Integer, BssidDisableReason> mBssidDisableReasons =
130             buildBssidDisableReasons();
131     private final SparseArray<DisableReasonInfo> mDisableReasonInfo;
132 
133     // Map of bssid to BssidStatus
134     private Map<String, BssidStatus> mBssidStatusMap = new ArrayMap<>();
135     private Set<String> mDisabledSsids = new ArraySet<>();
136 
137     // Internal logger to make sure imporatant logs do not get lost.
138     private BssidBlocklistMonitorLogger mBssidBlocklistMonitorLogger =
139             new BssidBlocklistMonitorLogger(60);
140 
141     // Map of ssid to Allowlist SSIDs
142     private Map<String, List<String>> mSsidAllowlistMap = new ArrayMap<>();
143     private Set<WifiSsid> mSsidsAllowlistForNetworkSelection = new ArraySet<>();
144 
145     /**
146      * Verbose logging flag. Toggled by developer options.
147      */
148     private boolean mVerboseLoggingEnabled = false;
149 
150 
buildBssidDisableReasons()151     private Map<Integer, BssidDisableReason> buildBssidDisableReasons() {
152         Map<Integer, BssidDisableReason> result = new ArrayMap<>();
153         result.put(REASON_AP_UNABLE_TO_HANDLE_NEW_STA, new BssidDisableReason(
154                 "REASON_AP_UNABLE_TO_HANDLE_NEW_STA", false, false));
155         result.put(REASON_NETWORK_VALIDATION_FAILURE, new BssidDisableReason(
156                 "REASON_NETWORK_VALIDATION_FAILURE", true, false));
157         result.put(REASON_WRONG_PASSWORD, new BssidDisableReason(
158                 "REASON_WRONG_PASSWORD", false, true));
159         result.put(REASON_EAP_FAILURE, new BssidDisableReason(
160                 "REASON_EAP_FAILURE", true, true));
161         result.put(REASON_ASSOCIATION_REJECTION, new BssidDisableReason(
162                 "REASON_ASSOCIATION_REJECTION", true, true));
163         result.put(REASON_ASSOCIATION_TIMEOUT, new BssidDisableReason(
164                 "REASON_ASSOCIATION_TIMEOUT", true, true));
165         result.put(REASON_AUTHENTICATION_FAILURE, new BssidDisableReason(
166                 "REASON_AUTHENTICATION_FAILURE", true, true));
167         result.put(REASON_DHCP_FAILURE, new BssidDisableReason(
168                 "REASON_DHCP_FAILURE", true, false));
169         result.put(REASON_ABNORMAL_DISCONNECT, new BssidDisableReason(
170                 "REASON_ABNORMAL_DISCONNECT", true, false));
171         result.put(REASON_FRAMEWORK_DISCONNECT_MBO_OCE, new BssidDisableReason(
172                 "REASON_FRAMEWORK_DISCONNECT_MBO_OCE", false, false));
173         result.put(REASON_FRAMEWORK_DISCONNECT_FAST_RECONNECT, new BssidDisableReason(
174                 "REASON_FRAMEWORK_DISCONNECT_FAST_RECONNECT", false, false));
175         result.put(REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE, new BssidDisableReason(
176                 "REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE", true, false));
177         // TODO: b/174166637, add the same reason code in SSID blocklist and mark ignoreIfOnlyBssid
178         // to true once it is covered in SSID blocklist.
179         result.put(REASON_NONLOCAL_DISCONNECT_CONNECTING, new BssidDisableReason(
180                 "REASON_NONLOCAL_DISCONNECT_CONNECTING", true, false));
181         return result;
182     }
183 
184     class BssidDisableReason {
185         public final String reasonString;
186         public final boolean isLowRssiSensitive;
187         public final boolean ignoreIfOnlyBssid;
188 
BssidDisableReason(String reasonString, boolean isLowRssiSensitive, boolean ignoreIfOnlyBssid)189         BssidDisableReason(String reasonString, boolean isLowRssiSensitive,
190                 boolean ignoreIfOnlyBssid) {
191             this.reasonString = reasonString;
192             this.isLowRssiSensitive = isLowRssiSensitive;
193             this.ignoreIfOnlyBssid = ignoreIfOnlyBssid;
194         }
195     }
196 
197     /**
198      * Create a new instance of WifiBlocklistMonitor
199      */
WifiBlocklistMonitor(Context context, WifiConnectivityHelper connectivityHelper, WifiLastResortWatchdog wifiLastResortWatchdog, Clock clock, LocalLog localLog, WifiScoreCard wifiScoreCard, ScoringParams scoringParams, WifiMetrics wifiMetrics, WifiPermissionsUtil wifiPermissionsUtil)200     WifiBlocklistMonitor(Context context, WifiConnectivityHelper connectivityHelper,
201             WifiLastResortWatchdog wifiLastResortWatchdog, Clock clock, LocalLog localLog,
202             WifiScoreCard wifiScoreCard, ScoringParams scoringParams, WifiMetrics wifiMetrics,
203             WifiPermissionsUtil wifiPermissionsUtil) {
204         mContext = context;
205         mConnectivityHelper = connectivityHelper;
206         mWifiLastResortWatchdog = wifiLastResortWatchdog;
207         mClock = clock;
208         mLocalLog = localLog;
209         mWifiScoreCard = wifiScoreCard;
210         mScoringParams = scoringParams;
211         mDisableReasonInfo = DISABLE_REASON_INFOS.clone();
212         mWifiMetrics = wifiMetrics;
213         mWifiPermissionsUtil = wifiPermissionsUtil;
214         loadCustomConfigsForDisableReasonInfos();
215     }
216 
217     // A helper to log debugging information in the local log buffer, which can
218     // be retrieved in bugreport.
localLog(String log)219     private void localLog(String log) {
220         mLocalLog.log(log);
221     }
222 
223     /**
224      * calculates the blocklist duration based on the current failure streak with exponential
225      * backoff.
226      * @param failureStreak should be greater or equal to 0.
227      * @return duration to block the BSSID in milliseconds
228      */
getBlocklistDurationWithExponentialBackoff(int failureStreak, int baseBlocklistDurationMs)229     private long getBlocklistDurationWithExponentialBackoff(int failureStreak,
230             int baseBlocklistDurationMs) {
231         failureStreak = Math.min(failureStreak, mContext.getResources().getInteger(
232                 R.integer.config_wifiBssidBlocklistMonitorFailureStreakCap));
233         if (failureStreak < 1) {
234             return baseBlocklistDurationMs;
235         }
236         return (long) (Math.pow(2.0, (double) failureStreak) * baseBlocklistDurationMs);
237     }
238 
239     /**
240      * Dump the local log buffer and other internal state of WifiBlocklistMonitor.
241      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)242     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
243         pw.println("Dump of WifiBlocklistMonitor");
244         mLocalLog.dump(fd, pw, args);
245         pw.println("WifiBlocklistMonitor - Bssid blocklist begin ----");
246         mBssidStatusMap.values().stream().forEach(entry -> pw.println(entry));
247         pw.println("WifiBlocklistMonitor - Bssid blocklist end ----");
248         mBssidBlocklistMonitorLogger.dump(pw);
249     }
250 
addToBlocklist(@onNull BssidStatus entry, long durationMs, @FailureReason int reason, int rssi)251     private void addToBlocklist(@NonNull BssidStatus entry, long durationMs,
252             @FailureReason int reason, int rssi) {
253         entry.setAsBlocked(durationMs, reason, rssi);
254         localLog(TAG + " addToBlocklist: bssid=" + entry.bssid + ", ssid=" + entry.ssid
255                 + ", durationMs=" + durationMs + ", reason=" + getFailureReasonString(reason)
256                 + ", rssi=" + rssi);
257     }
258 
259     /**
260      * increments the number of failures for the given bssid and returns the number of failures so
261      * far.
262      * @return the BssidStatus for the BSSID
263      */
incrementFailureCountForBssid( @onNull String bssid, @NonNull String ssid, int reasonCode)264     private @NonNull BssidStatus incrementFailureCountForBssid(
265             @NonNull String bssid, @NonNull String ssid, int reasonCode) {
266         BssidStatus status = getOrCreateBssidStatus(bssid, ssid);
267         status.incrementFailureCount(reasonCode);
268         return status;
269     }
270 
271     /**
272      * Get the BssidStatus representing the BSSID or create a new one if it doesn't exist.
273      */
getOrCreateBssidStatus(@onNull String bssid, @NonNull String ssid)274     private @NonNull BssidStatus getOrCreateBssidStatus(@NonNull String bssid,
275             @NonNull String ssid) {
276         BssidStatus status = mBssidStatusMap.get(bssid);
277         if (status == null || !ssid.equals(status.ssid)) {
278             if (status != null) {
279                 localLog("getOrCreateBssidStatus: BSSID=" + bssid + ", SSID changed from "
280                         + status.ssid + " to " + ssid);
281             }
282             status = new BssidStatus(bssid, ssid);
283             mBssidStatusMap.put(bssid, status);
284         }
285         return status;
286     }
287 
288     /**
289      * Set a list of SSIDs that will always be enabled for network selection.
290      */
setSsidsAllowlist(@onNull List<WifiSsid> ssids)291     public void setSsidsAllowlist(@NonNull List<WifiSsid> ssids) {
292         mSsidsAllowlistForNetworkSelection = new ArraySet<>(ssids);
293     }
294 
295     /**
296      * Get the list of SSIDs that will always be enabled for network selection.
297      */
getSsidsAllowlist()298     public List<WifiSsid> getSsidsAllowlist() {
299         return new ArrayList<>(mSsidsAllowlistForNetworkSelection);
300     }
301 
isValidNetworkAndFailureReasonForBssidBlocking(String bssid, WifiConfiguration config, @FailureReason int reasonCode)302     private boolean isValidNetworkAndFailureReasonForBssidBlocking(String bssid,
303             WifiConfiguration config, @FailureReason int reasonCode) {
304         if (bssid == null || config == null
305                 || bssid.equals(ClientModeImpl.SUPPLICANT_BSSID_ANY)
306                 || reasonCode < 0 || reasonCode >= NUMBER_REASON_CODES) {
307             Log.e(TAG, "Invalid input: BSSID=" + bssid + ", config=" + config
308                     + ", reasonCode=" + reasonCode);
309             return false;
310         }
311         return !isConfigExemptFromBlocklist(config);
312     }
313 
isConfigExemptFromBlocklist(@onNull WifiConfiguration config)314     private boolean isConfigExemptFromBlocklist(@NonNull WifiConfiguration config) {
315         try {
316             // Only enterprise owned configs that are in the doNoBlocklist are exempt from
317             // blocklisting.
318             WifiSsid wifiSsid = WifiSsid.fromString(config.SSID);
319             return mSsidsAllowlistForNetworkSelection.contains(wifiSsid)
320                     && mWifiPermissionsUtil.isAdmin(config.creatorUid, config.creatorName);
321         } catch (IllegalArgumentException e) {
322             Log.e(TAG, "Failed to convert raw ssid=" + config.SSID + " to WifiSsid");
323             return false;
324         }
325     }
326 
shouldWaitForWatchdogToTriggerFirst(String bssid, @FailureReason int reasonCode)327     private boolean shouldWaitForWatchdogToTriggerFirst(String bssid,
328             @FailureReason int reasonCode) {
329         boolean isWatchdogRelatedFailure = reasonCode == REASON_ASSOCIATION_REJECTION
330                 || reasonCode == REASON_AUTHENTICATION_FAILURE
331                 || reasonCode == REASON_DHCP_FAILURE;
332         return isWatchdogRelatedFailure && mWifiLastResortWatchdog.shouldIgnoreBssidUpdate(bssid);
333     }
334 
335     /**
336      * Block any attempts to auto-connect to the BSSID for the specified duration.
337      * This is meant to be used by features that need wifi to avoid a BSSID for a certain duration,
338      * and thus will not increase the failure streak counters.
339      * @param bssid identifies the AP to block.
340      * @param config identifies the WifiConfiguration.
341      * @param durationMs duration in millis to block.
342      * @param blockReason reason for blocking the BSSID.
343      * @param rssi the latest RSSI observed.
344      */
blockBssidForDurationMs(@onNull String bssid, WifiConfiguration config, long durationMs, @FailureReason int blockReason, int rssi)345     public void blockBssidForDurationMs(@NonNull String bssid, WifiConfiguration config,
346             long durationMs, @FailureReason int blockReason, int rssi) {
347         if (durationMs <= 0 || !isValidNetworkAndFailureReasonForBssidBlocking(
348                 bssid, config, blockReason)) {
349             Log.e(TAG, "Invalid input: BSSID=" + bssid + ", config=" + config
350                     + ", durationMs=" + durationMs + ", blockReason=" + blockReason
351                     + ", rssi=" + rssi);
352             return;
353         }
354         BssidStatus status = getOrCreateBssidStatus(bssid, config.SSID);
355         if (status.isInBlocklist
356                 && status.blocklistEndTimeMs - mClock.getWallClockMillis() > durationMs) {
357             // Return because this BSSID is already being blocked for a longer time.
358             return;
359         }
360         addToBlocklist(status, durationMs, blockReason, rssi);
361     }
362 
getFailureReasonString(@ailureReason int reasonCode)363     private String getFailureReasonString(@FailureReason int reasonCode) {
364         if (reasonCode == INVALID_REASON) {
365             return "INVALID_REASON";
366         }
367         BssidDisableReason disableReason = mBssidDisableReasons.get(reasonCode);
368         if (disableReason == null) {
369             return "REASON_UNKNOWN";
370         }
371         return disableReason.reasonString;
372     }
373 
getFailureThresholdForReason(@ailureReason int reasonCode)374     private int getFailureThresholdForReason(@FailureReason int reasonCode) {
375         if (mFailureCountDisableThresholdArrayInitialized) {
376             return FAILURE_COUNT_DISABLE_THRESHOLD[reasonCode];
377         }
378         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_AP_UNABLE_TO_HANDLE_NEW_STA] =
379                 mContext.getResources().getInteger(
380                         R.integer.config_wifiBssidBlocklistMonitorApUnableToHandleNewStaThreshold);
381         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_NETWORK_VALIDATION_FAILURE] =
382                 mContext.getResources().getInteger(R.integer
383                         .config_wifiBssidBlocklistMonitorNetworkValidationFailureThreshold);
384         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_WRONG_PASSWORD] =
385                 mContext.getResources().getInteger(
386                         R.integer.config_wifiBssidBlocklistMonitorWrongPasswordThreshold);
387         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_EAP_FAILURE] =
388                 mContext.getResources().getInteger(
389                         R.integer.config_wifiBssidBlocklistMonitorEapFailureThreshold);
390         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_ASSOCIATION_REJECTION] =
391                 mContext.getResources().getInteger(
392                         R.integer.config_wifiBssidBlocklistMonitorAssociationRejectionThreshold);
393         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_ASSOCIATION_TIMEOUT] =
394                 mContext.getResources().getInteger(
395                         R.integer.config_wifiBssidBlocklistMonitorAssociationTimeoutThreshold);
396         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_AUTHENTICATION_FAILURE] =
397                 mContext.getResources().getInteger(
398                         R.integer.config_wifiBssidBlocklistMonitorAuthenticationFailureThreshold);
399         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_DHCP_FAILURE] =
400                 mContext.getResources().getInteger(
401                         R.integer.config_wifiBssidBlocklistMonitorDhcpFailureThreshold);
402         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_ABNORMAL_DISCONNECT] =
403                 mContext.getResources().getInteger(
404                         R.integer.config_wifiBssidBlocklistMonitorAbnormalDisconnectThreshold);
405         FAILURE_COUNT_DISABLE_THRESHOLD[REASON_NONLOCAL_DISCONNECT_CONNECTING] =
406                 mContext.getResources().getInteger(R.integer
407                         .config_wifiBssidBlocklistMonitorNonlocalDisconnectConnectingThreshold);
408         mFailureCountDisableThresholdArrayInitialized = true;
409         return FAILURE_COUNT_DISABLE_THRESHOLD[reasonCode];
410     }
411 
handleBssidConnectionFailureInternal(String bssid, String ssid, @FailureReason int reasonCode, int rssi)412     private boolean handleBssidConnectionFailureInternal(String bssid, String ssid,
413             @FailureReason int reasonCode, int rssi) {
414         BssidStatus entry = incrementFailureCountForBssid(bssid, ssid, reasonCode);
415         int failureThreshold = getFailureThresholdForReason(reasonCode);
416         int currentStreak = mWifiScoreCard.getBssidBlocklistStreak(ssid, bssid, reasonCode);
417         if (currentStreak > 0 || entry.failureCount[reasonCode] >= failureThreshold) {
418             // To rule out potential device side issues, don't add to blocklist if
419             // WifiLastResortWatchdog is still not triggered
420             if (shouldWaitForWatchdogToTriggerFirst(bssid, reasonCode)) {
421                 localLog("Ignoring failure to wait for watchdog to trigger first.");
422                 return false;
423             }
424             int baseBlockDurationMs = getBaseBlockDurationForReason(reasonCode);
425             addToBlocklist(entry,
426                     getBlocklistDurationWithExponentialBackoff(currentStreak, baseBlockDurationMs),
427                     reasonCode, rssi);
428             mWifiScoreCard.incrementBssidBlocklistStreak(ssid, bssid, reasonCode);
429             return true;
430         }
431         return false;
432     }
433 
getBaseBlockDurationForReason(int blockReason)434     private int getBaseBlockDurationForReason(int blockReason) {
435         switch (blockReason) {
436             case REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE:
437                 return mContext.getResources().getInteger(R.integer
438                         .config_wifiBssidBlocklistMonitorConnectedScoreBaseBlockDurationMs);
439             default:
440                 return mContext.getResources().getInteger(
441                         R.integer.config_wifiBssidBlocklistMonitorBaseBlockDurationMs);
442         }
443     }
444 
445     /**
446      * Note a failure event on a bssid and perform appropriate actions.
447      * @return True if the blocklist has been modified.
448      */
handleBssidConnectionFailure(String bssid, WifiConfiguration config, @FailureReason int reasonCode, int rssi)449     public boolean handleBssidConnectionFailure(String bssid, WifiConfiguration config,
450             @FailureReason int reasonCode, int rssi) {
451         if (!isValidNetworkAndFailureReasonForBssidBlocking(bssid, config, reasonCode)) {
452             return false;
453         }
454         String ssid = config.SSID;
455         BssidDisableReason bssidDisableReason = mBssidDisableReasons.get(reasonCode);
456         if (bssidDisableReason == null) {
457             Log.e(TAG, "Bssid disable reason not found. ReasonCode=" + reasonCode);
458             return false;
459         }
460         if (bssidDisableReason.ignoreIfOnlyBssid && !mDisabledSsids.contains(ssid)
461                 && mWifiLastResortWatchdog.isBssidOnlyApOfSsid(bssid)) {
462             localLog("Ignoring BSSID failure due to no other APs available. BSSID=" + bssid);
463             return false;
464         }
465         if (reasonCode == REASON_ABNORMAL_DISCONNECT) {
466             long connectionTime = mWifiScoreCard.getBssidConnectionTimestampMs(ssid, bssid);
467             // only count disconnects that happen shortly after a connection.
468             if (mClock.getWallClockMillis() - connectionTime
469                     > mContext.getResources().getInteger(
470                             R.integer.config_wifiBssidBlocklistAbnormalDisconnectTimeWindowMs)) {
471                 return false;
472             }
473         }
474         return handleBssidConnectionFailureInternal(bssid, ssid, reasonCode, rssi);
475     }
476 
477     /**
478      * To be called when a WifiConfiguration is either temporarily disabled or permanently disabled.
479      * @param ssid of the WifiConfiguration that is disabled.
480      */
handleWifiConfigurationDisabled(String ssid)481     public void handleWifiConfigurationDisabled(String ssid) {
482         if (ssid != null) {
483             mDisabledSsids.add(ssid);
484         }
485     }
486 
487     /**
488      * Note a connection success event on a bssid and clear appropriate failure counters.
489      */
handleBssidConnectionSuccess(@onNull String bssid, @NonNull String ssid)490     public void handleBssidConnectionSuccess(@NonNull String bssid, @NonNull String ssid) {
491         mDisabledSsids.remove(ssid);
492         /**
493          * First reset the blocklist streak.
494          * This needs to be done even if a BssidStatus is not found, since the BssidStatus may
495          * have been removed due to blocklist timeout.
496          */
497         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_AP_UNABLE_TO_HANDLE_NEW_STA);
498         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_WRONG_PASSWORD);
499         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_EAP_FAILURE);
500         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_ASSOCIATION_REJECTION);
501         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_ASSOCIATION_TIMEOUT);
502         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_AUTHENTICATION_FAILURE);
503         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid,
504                 REASON_NONLOCAL_DISCONNECT_CONNECTING);
505 
506         long connectionTime = mClock.getWallClockMillis();
507         long prevConnectionTime = mWifiScoreCard.setBssidConnectionTimestampMs(
508                 ssid, bssid, connectionTime);
509         if (connectionTime - prevConnectionTime > ABNORMAL_DISCONNECT_RESET_TIME_MS) {
510             mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_ABNORMAL_DISCONNECT);
511             mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid,
512                     REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE);
513         }
514 
515         BssidStatus status = mBssidStatusMap.get(bssid);
516         if (status == null) {
517             return;
518         }
519         // Clear the L2 failure counters
520         status.failureCount[REASON_AP_UNABLE_TO_HANDLE_NEW_STA] = 0;
521         status.failureCount[REASON_WRONG_PASSWORD] = 0;
522         status.failureCount[REASON_EAP_FAILURE] = 0;
523         status.failureCount[REASON_ASSOCIATION_REJECTION] = 0;
524         status.failureCount[REASON_ASSOCIATION_TIMEOUT] = 0;
525         status.failureCount[REASON_AUTHENTICATION_FAILURE] = 0;
526         status.failureCount[REASON_NONLOCAL_DISCONNECT_CONNECTING] = 0;
527         if (connectionTime - prevConnectionTime > ABNORMAL_DISCONNECT_RESET_TIME_MS) {
528             status.failureCount[REASON_ABNORMAL_DISCONNECT] = 0;
529         }
530     }
531 
532     /**
533      * Note a successful network validation on a BSSID and clear appropriate failure counters.
534      * And then remove the BSSID from blocklist.
535      */
handleNetworkValidationSuccess(@onNull String bssid, @NonNull String ssid)536     public void handleNetworkValidationSuccess(@NonNull String bssid, @NonNull String ssid) {
537         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_NETWORK_VALIDATION_FAILURE);
538         BssidStatus status = mBssidStatusMap.get(bssid);
539         if (status == null) {
540             return;
541         }
542         status.failureCount[REASON_NETWORK_VALIDATION_FAILURE] = 0;
543         /**
544          * Network validation may take more than 1 tries to succeed.
545          * remove the BSSID from blocklist to make sure we are not accidentally blocking good
546          * BSSIDs.
547          **/
548         if (status.isInBlocklist) {
549             mBssidBlocklistMonitorLogger.logBssidUnblocked(status, "Network validation success");
550             mBssidStatusMap.remove(bssid);
551         }
552     }
553 
554     /**
555      * Note a successful DHCP provisioning and clear appropriate faliure counters.
556      */
handleDhcpProvisioningSuccess(@onNull String bssid, @NonNull String ssid)557     public void handleDhcpProvisioningSuccess(@NonNull String bssid, @NonNull String ssid) {
558         mWifiScoreCard.resetBssidBlocklistStreak(ssid, bssid, REASON_DHCP_FAILURE);
559         BssidStatus status = mBssidStatusMap.get(bssid);
560         if (status == null) {
561             return;
562         }
563         status.failureCount[REASON_DHCP_FAILURE] = 0;
564     }
565 
566     /**
567      * Note the removal of a network from the Wifi stack's internal database and reset
568      * appropriate failure counters.
569      * @param ssid
570      */
handleNetworkRemoved(@onNull String ssid)571     public void handleNetworkRemoved(@NonNull String ssid) {
572         clearBssidBlocklistForSsid(ssid);
573         mWifiScoreCard.resetBssidBlocklistStreakForSsid(ssid);
574     }
575 
576     /**
577      * Clears the blocklist for BSSIDs associated with the input SSID only.
578      * @param ssid
579      */
clearBssidBlocklistForSsid(@onNull String ssid)580     public void clearBssidBlocklistForSsid(@NonNull String ssid) {
581         int prevSize = mBssidStatusMap.size();
582         mBssidStatusMap.entrySet().removeIf(e -> {
583             BssidStatus status = e.getValue();
584             if (status.ssid == null) {
585                 return false;
586             }
587             if (status.ssid.equals(ssid)) {
588                 mBssidBlocklistMonitorLogger.logBssidUnblocked(
589                         status, "clearBssidBlocklistForSsid");
590                 return true;
591             }
592             return false;
593         });
594         int diff = prevSize - mBssidStatusMap.size();
595         if (diff > 0) {
596             localLog(TAG + " clearBssidBlocklistForSsid: SSID=" + ssid
597                     + ", num BSSIDs cleared=" + diff);
598         }
599     }
600 
601     /**
602      * Clears the BSSID blocklist and failure counters.
603      */
clearBssidBlocklist()604     public void clearBssidBlocklist() {
605         if (mBssidStatusMap.size() > 0) {
606             int prevSize = mBssidStatusMap.size();
607             for (BssidStatus status : mBssidStatusMap.values()) {
608                 mBssidBlocklistMonitorLogger.logBssidUnblocked(status, "clearBssidBlocklist");
609             }
610             mBssidStatusMap.clear();
611             localLog(TAG + " clearBssidBlocklist: num BSSIDs cleared="
612                     + (prevSize - mBssidStatusMap.size()));
613         }
614         mDisabledSsids.clear();
615     }
616 
617     /**
618      * @param ssid
619      * @return the number of BSSIDs currently in the blocklist for the |ssid|.
620      */
updateAndGetNumBlockedBssidsForSsid(@onNull String ssid)621     public int updateAndGetNumBlockedBssidsForSsid(@NonNull String ssid) {
622         return (int) updateAndGetBssidBlocklistInternal()
623                 .filter(entry -> ssid.equals(entry.ssid)).count();
624     }
625 
getNumBlockedBssidsForSsids(@onNull Set<String> ssids)626     private int getNumBlockedBssidsForSsids(@NonNull Set<String> ssids) {
627         if (ssids.isEmpty()) {
628             return 0;
629         }
630         return (int) mBssidStatusMap.values().stream()
631                 .filter(entry -> entry.isInBlocklist && ssids.contains(entry.ssid))
632                 .count();
633     }
634 
635     /**
636      * Overloaded version of updateAndGetBssidBlocklist.
637      * Accepts a @Nullable String ssid as input, and updates the firmware roaming
638      * configuration if the blocklist for the input ssid has been changed.
639      * @param ssids set of ssids to update firmware roaming configuration for.
640      * @return Set of BSSIDs currently in the blocklist
641      */
updateAndGetBssidBlocklistForSsids(@onNull Set<String> ssids)642     public Set<String> updateAndGetBssidBlocklistForSsids(@NonNull Set<String> ssids) {
643         int numBefore = getNumBlockedBssidsForSsids(ssids);
644         Set<String> bssidBlocklist = updateAndGetBssidBlocklist();
645         if (getNumBlockedBssidsForSsids(ssids) != numBefore) {
646             updateFirmwareRoamingConfiguration(ssids);
647         }
648         return bssidBlocklist;
649     }
650 
651     /**
652      * Gets the BSSIDs that are currently in the blocklist.
653      * @return Set of BSSIDs currently in the blocklist
654      */
updateAndGetBssidBlocklist()655     public Set<String> updateAndGetBssidBlocklist() {
656         return updateAndGetBssidBlocklistInternal()
657                 .map(entry -> entry.bssid)
658                 .collect(Collectors.toSet());
659     }
660 
661     /**
662      * Gets the list of block reasons for BSSIDs currently in the blocklist.
663      * @return The set of unique reasons for blocking BSSIDs with this SSID.
664      */
getFailureReasonsForSsid(@onNull String ssid)665     public Set<Integer> getFailureReasonsForSsid(@NonNull String ssid) {
666         if (ssid == null) {
667             return Collections.emptySet();
668         }
669         return mBssidStatusMap.values().stream()
670                 .filter(entry -> entry.isInBlocklist && ssid.equals(entry.ssid))
671                 .map(entry -> entry.blockReason)
672                 .collect(Collectors.toSet());
673     }
674 
675     /**
676      * Attempts to re-enable BSSIDs that likely experienced failures due to low RSSI.
677      * @param scanDetails
678      * @return the list of ScanDetails for which BSSIDs were re-enabled.
679      */
tryEnablingBlockedBssids(List<ScanDetail> scanDetails)680     public @NonNull List<ScanDetail> tryEnablingBlockedBssids(List<ScanDetail> scanDetails) {
681         if (scanDetails == null) {
682             return Collections.EMPTY_LIST;
683         }
684         List<ScanDetail> results = new ArrayList<>();
685         for (ScanDetail scanDetail : scanDetails) {
686             ScanResult scanResult = scanDetail.getScanResult();
687             if (scanResult == null) {
688                 continue;
689             }
690             BssidStatus status = mBssidStatusMap.get(scanResult.BSSID);
691             if (status == null || !status.isInBlocklist
692                     || !isLowRssiSensitiveFailure(status.blockReason)) {
693                 continue;
694             }
695             int sufficientRssi = mScoringParams.getSufficientRssi(scanResult.frequency);
696             int goodRssi = mScoringParams.getGoodRssi(scanResult.frequency);
697             boolean rssiMinDiffAchieved = scanResult.level - status.lastRssi
698                     >= MIN_RSSI_DIFF_TO_UNBLOCK_BSSID;
699             boolean sufficientRssiBreached =
700                     status.lastRssi < sufficientRssi && scanResult.level >= sufficientRssi;
701             boolean goodRssiBreached = status.lastRssi < goodRssi && scanResult.level >= goodRssi;
702             if (rssiMinDiffAchieved && (sufficientRssiBreached || goodRssiBreached)) {
703                 mBssidBlocklistMonitorLogger.logBssidUnblocked(
704                         status, "rssi significantly improved");
705                 mBssidStatusMap.remove(status.bssid);
706                 results.add(scanDetail);
707             }
708         }
709         return results;
710     }
711 
isLowRssiSensitiveFailure(int blockReason)712     private boolean isLowRssiSensitiveFailure(int blockReason) {
713         return mBssidDisableReasons.get(blockReason) == null ? false
714                 : mBssidDisableReasons.get(blockReason).isLowRssiSensitive;
715     }
716 
717     /**
718      * Removes expired BssidStatus entries and then return remaining entries in the blocklist.
719      * @return Stream of BssidStatus for BSSIDs that are in the blocklist.
720      */
updateAndGetBssidBlocklistInternal()721     private Stream<BssidStatus> updateAndGetBssidBlocklistInternal() {
722         Stream.Builder<BssidStatus> builder = Stream.builder();
723         long curTime = mClock.getWallClockMillis();
724         mBssidStatusMap.entrySet().removeIf(e -> {
725             BssidStatus status = e.getValue();
726             if (status.isInBlocklist) {
727                 if (status.blocklistEndTimeMs < curTime) {
728                     mBssidBlocklistMonitorLogger.logBssidUnblocked(
729                             status, "updateAndGetBssidBlocklistInternal");
730                     return true;
731                 }
732                 builder.accept(status);
733             }
734             return false;
735         });
736         return builder.build();
737     }
738 
739     /**
740      * Sends the BSSIDs belonging to the input SSID down to the firmware to prevent auto-roaming
741      * to those BSSIDs.
742      * @param ssids
743      */
updateFirmwareRoamingConfiguration(@onNull Set<String> ssids)744     public void updateFirmwareRoamingConfiguration(@NonNull Set<String> ssids) {
745         if (!mConnectivityHelper.isFirmwareRoamingSupported()) {
746             return;
747         }
748         ArrayList<String> bssidBlocklist = updateAndGetBssidBlocklistInternal()
749                 .filter(entry -> ssids.contains(entry.ssid))
750                 .sorted((o1, o2) -> (int) (o2.blocklistEndTimeMs - o1.blocklistEndTimeMs))
751                 .map(entry -> entry.bssid)
752                 .collect(Collectors.toCollection(ArrayList::new));
753         int fwMaxBlocklistSize = mConnectivityHelper.getMaxNumBlocklistBssid();
754         if (fwMaxBlocklistSize <= 0) {
755             Log.e(TAG, "Invalid max BSSID blocklist size:  " + fwMaxBlocklistSize);
756             return;
757         }
758         // Having the blocklist size exceeding firmware max limit is unlikely because we have
759         // already flitered based on SSID. But just in case this happens, we are prioritizing
760         // sending down BSSIDs blocked for the longest time.
761         if (bssidBlocklist.size() > fwMaxBlocklistSize) {
762             bssidBlocklist = new ArrayList<String>(bssidBlocklist.subList(0,
763                     fwMaxBlocklistSize));
764         }
765 
766         // Collect all the allowed SSIDs
767         Set<String> allowedSsidSet = new HashSet<>();
768         for (String ssid : ssids) {
769             List<String> allowedSsidsForSsid = mSsidAllowlistMap.get(ssid);
770             if (allowedSsidsForSsid != null) {
771                 allowedSsidSet.addAll(allowedSsidsForSsid);
772             }
773         }
774         ArrayList<String> ssidAllowlist = new ArrayList<>(allowedSsidSet);
775         int allowlistSize = ssidAllowlist.size();
776         int maxAllowlistSize = mConnectivityHelper.getMaxNumAllowlistSsid();
777         if (maxAllowlistSize <= 0) {
778             Log.wtf(TAG, "Invalid max SSID allowlist size:  " + maxAllowlistSize);
779             return;
780         }
781         if (allowlistSize > maxAllowlistSize) {
782             ssidAllowlist = new ArrayList<>(ssidAllowlist.subList(0, maxAllowlistSize));
783             localLog("Trim down SSID allowlist size from " + allowlistSize + " to "
784                     + ssidAllowlist.size());
785         }
786 
787         // plumb down to HAL
788         String message = "set firmware roaming configurations. "
789                 + "bssidBlocklist=";
790         if (bssidBlocklist.size() == 0) {
791             message += "<EMPTY>";
792         } else {
793             message += String.join(", ", bssidBlocklist);
794         }
795         if (!mConnectivityHelper.setFirmwareRoamingConfiguration(bssidBlocklist, ssidAllowlist)) {
796             Log.e(TAG, "Failed to " + message);
797             mBssidBlocklistMonitorLogger.log("Failed to " + message);
798         } else {
799             mBssidBlocklistMonitorLogger.log("Successfully " + message);
800         }
801     }
802 
803     @VisibleForTesting
getBssidBlocklistMonitorLoggerSize()804     public int getBssidBlocklistMonitorLoggerSize() {
805         return mBssidBlocklistMonitorLogger.size();
806     }
807 
808     private class BssidBlocklistMonitorLogger {
809         private LinkedList<String> mLogBuffer = new LinkedList<>();
810         private int mBufferSize;
811 
BssidBlocklistMonitorLogger(int bufferSize)812         BssidBlocklistMonitorLogger(int bufferSize) {
813             mBufferSize = bufferSize;
814         }
815 
logBssidUnblocked(BssidStatus bssidStatus, String unblockReason)816         public void logBssidUnblocked(BssidStatus bssidStatus, String unblockReason) {
817             // only log history for Bssids that had been blocked.
818             if (bssidStatus == null || !bssidStatus.isInBlocklist) {
819                 return;
820             }
821             StringBuilder sb = createStringBuilderWithLogTime();
822             sb.append(", Bssid unblocked, Reason=" + unblockReason);
823             sb.append(", Unblocked BssidStatus={" + bssidStatus.toString() + "}");
824             logInternal(sb.toString());
825         }
826 
827         // cache a single line of log message in the rotating buffer
log(String message)828         public void log(String message) {
829             if (message == null) {
830                 return;
831             }
832             StringBuilder sb = createStringBuilderWithLogTime();
833             sb.append(" " + message);
834             logInternal(sb.toString());
835         }
836 
createStringBuilderWithLogTime()837         private StringBuilder createStringBuilderWithLogTime() {
838             StringBuilder sb = new StringBuilder();
839             Calendar c = Calendar.getInstance();
840             c.setTimeInMillis(mClock.getWallClockMillis());
841             sb.append("logTime=").append(c.get(Calendar.MONTH)).append("-")
842                     .append(c.get(Calendar.DAY_OF_MONTH)).append(" ")
843                     .append(c.get(Calendar.HOUR_OF_DAY)).append(":")
844                     .append(c.get(Calendar.MINUTE)).append(":")
845                     .append(c.get(Calendar.SECOND)).append(".")
846                     .append(c.get(Calendar.MILLISECOND));
847             return sb;
848         }
849 
logInternal(String message)850         private void logInternal(String message) {
851             mLogBuffer.add(message);
852             if (mLogBuffer.size() > mBufferSize) {
853                 mLogBuffer.removeFirst();
854             }
855         }
856 
857         @VisibleForTesting
size()858         public int size() {
859             return mLogBuffer.size();
860         }
861 
dump(PrintWriter pw)862         public void dump(PrintWriter pw) {
863             pw.println("WifiBlocklistMonitor - Bssid blocklist logs begin ----");
864             for (String line : mLogBuffer) {
865                 pw.println(line);
866             }
867             pw.println("List of SSIDs to never block:");
868             for (WifiSsid ssid : mSsidsAllowlistForNetworkSelection) {
869                 pw.println(ssid.toString());
870             }
871             pw.println("WifiBlocklistMonitor - Bssid blocklist logs end ----");
872         }
873     }
874 
875     /**
876      * Helper class that counts the number of failures per BSSID.
877      */
878     private class BssidStatus {
879         public final String bssid;
880         public final String ssid;
881         public final int[] failureCount = new int[NUMBER_REASON_CODES];
882         public int blockReason = INVALID_REASON; // reason of blocking this BSSID
883         // The latest RSSI that's seen before this BSSID is added to blocklist.
884         public int lastRssi = 0;
885 
886         // The following are used to flag how long this BSSID stays in the blocklist.
887         public boolean isInBlocklist;
888         public long blocklistEndTimeMs;
889         public long blocklistStartTimeMs;
890 
BssidStatus(String bssid, String ssid)891         BssidStatus(String bssid, String ssid) {
892             this.bssid = bssid;
893             this.ssid = ssid;
894         }
895 
896         /**
897          * increments the failure count for the reasonCode by 1.
898          * @return the incremented failure count
899          */
incrementFailureCount(int reasonCode)900         public int incrementFailureCount(int reasonCode) {
901             return ++failureCount[reasonCode];
902         }
903 
904         /**
905          * Set this BSSID as blocked for the specified duration.
906          * @param durationMs
907          * @param blockReason
908          * @param rssi
909          */
setAsBlocked(long durationMs, @FailureReason int blockReason, int rssi)910         public void setAsBlocked(long durationMs, @FailureReason int blockReason, int rssi) {
911             isInBlocklist = true;
912             blocklistStartTimeMs = mClock.getWallClockMillis();
913             blocklistEndTimeMs = blocklistStartTimeMs + durationMs;
914             this.blockReason = blockReason;
915             lastRssi = rssi;
916             mWifiMetrics.incrementBssidBlocklistCount(blockReason);
917         }
918 
919         @Override
toString()920         public String toString() {
921             StringBuilder sb = new StringBuilder();
922             sb.append("BSSID=" + bssid);
923             sb.append(", SSID=" + ssid);
924             sb.append(", isInBlocklist=" + isInBlocklist);
925             if (isInBlocklist) {
926                 sb.append(", blockReason=" + getFailureReasonString(blockReason));
927                 sb.append(", lastRssi=" + lastRssi);
928                 Calendar c = Calendar.getInstance();
929                 c.setTimeInMillis(blocklistStartTimeMs);
930                 sb.append(", blocklistStartTime=").append(c.get(Calendar.MONTH)).append("-")
931                         .append(c.get(Calendar.DAY_OF_MONTH)).append(" ")
932                         .append(c.get(Calendar.HOUR_OF_DAY)).append(":")
933                         .append(c.get(Calendar.MINUTE)).append(":")
934                         .append(c.get(Calendar.SECOND)).append(".")
935                         .append(c.get(Calendar.MILLISECOND));
936                 c.setTimeInMillis(blocklistEndTimeMs);
937                 sb.append(", blocklistEndTime=").append(c.get(Calendar.MONTH)).append("-")
938                         .append(c.get(Calendar.DAY_OF_MONTH)).append(" ")
939                         .append(c.get(Calendar.HOUR_OF_DAY)).append(":")
940                         .append(c.get(Calendar.MINUTE)).append(":")
941                         .append(c.get(Calendar.SECOND)).append(".")
942                         .append(c.get(Calendar.MILLISECOND));
943             }
944             return sb.toString();
945         }
946     }
947 
948     /**
949      * Enable/disable verbose logging in WifiBlocklistMonitor.
950      */
enableVerboseLogging(boolean verbose)951     public void enableVerboseLogging(boolean verbose) {
952         mVerboseLoggingEnabled = verbose;
953     }
954 
955     /**
956      * Modify the internal copy of DisableReasonInfo with custom configurations defined in
957      * an overlay.
958      */
loadCustomConfigsForDisableReasonInfos()959     private void loadCustomConfigsForDisableReasonInfos() {
960         mDisableReasonInfo.put(NetworkSelectionStatus.DISABLED_ASSOCIATION_REJECTION,
961                 new DisableReasonInfo(
962                         // Note that there is a space at the end of this string. Cannot fix
963                         // since this string is persisted.
964                         "NETWORK_SELECTION_DISABLED_ASSOCIATION_REJECTION ",
965                         mContext.getResources().getInteger(R.integer
966                                 .config_wifiDisableReasonAssociationRejectionThreshold),
967                         5 * 60 * 1000));
968 
969         mDisableReasonInfo.put(NetworkSelectionStatus.DISABLED_AUTHENTICATION_FAILURE,
970                 new DisableReasonInfo(
971                         "NETWORK_SELECTION_DISABLED_AUTHENTICATION_FAILURE",
972                         mContext.getResources().getInteger(R.integer
973                                 .config_wifiDisableReasonAuthenticationFailureThreshold),
974                         5 * 60 * 1000));
975 
976         mDisableReasonInfo.put(NetworkSelectionStatus.DISABLED_DHCP_FAILURE,
977                 new DisableReasonInfo(
978                         "NETWORK_SELECTION_DISABLED_DHCP_FAILURE",
979                         mContext.getResources().getInteger(R.integer
980                                 .config_wifiDisableReasonDhcpFailureThreshold),
981                         5 * 60 * 1000));
982 
983         mDisableReasonInfo.put(NetworkSelectionStatus.DISABLED_NETWORK_NOT_FOUND,
984                 new DisableReasonInfo(
985                         "NETWORK_SELECTION_DISABLED_NETWORK_NOT_FOUND",
986                         mContext.getResources().getInteger(R.integer
987                                 .config_wifiDisableReasonNetworkNotFoundThreshold),
988                         5 * 60 * 1000));
989     }
990 
991     /** Update DisableReasonInfo with carrier configurations defined in an overlay. **/
loadCarrierConfigsForDisableReasonInfos()992     public void loadCarrierConfigsForDisableReasonInfos() {
993         int duration = mContext.getResources().getInteger(
994                 R.integer.config_wifiDisableReasonAuthenticationFailureCarrierSpecificDurationMs);
995         DisableReasonInfo disableReasonInfo = new DisableReasonInfo(
996                 "NETWORK_SELECTION_DISABLED_AUTHENTICATION_PRIVATE_EAP_ERROR",
997                 mContext.getResources().getInteger(R.integer
998                         .config_wifiDisableReasonAuthenticationFailureCarrierSpecificThreshold),
999                 duration);
1000         mDisableReasonInfo.put(
1001                 NetworkSelectionStatus.DISABLED_AUTHENTICATION_PRIVATE_EAP_ERROR,
1002                 disableReasonInfo);
1003     }
1004 
1005     /**
1006      * Returns true if the disable duration for this WifiConfiguration has passed. Returns false
1007      * if the WifiConfiguration is either not disabled or is permanently disabled.
1008      */
shouldEnableNetwork(WifiConfiguration config)1009     public boolean shouldEnableNetwork(WifiConfiguration config) {
1010         NetworkSelectionStatus networkStatus = config.getNetworkSelectionStatus();
1011         if (networkStatus.isNetworkTemporaryDisabled()) {
1012             long timeDifferenceMs =
1013                     mClock.getElapsedSinceBootMillis() - networkStatus.getDisableTime();
1014             int disableReason = networkStatus.getNetworkSelectionDisableReason();
1015             long disableTimeoutMs = (long) getNetworkSelectionDisableTimeoutMillis(disableReason);
1016             int exponentialBackoffCount = mWifiScoreCard.lookupNetwork(config.SSID)
1017                     .getRecentStats().getCount(WifiScoreCard.CNT_CONSECUTIVE_CONNECTION_FAILURE)
1018                     - NUM_CONSECUTIVE_FAILURES_PER_NETWORK_EXP_BACKOFF;
1019             for (int i = 0; i < exponentialBackoffCount; i++) {
1020                 disableTimeoutMs *= 2;
1021                 if (disableTimeoutMs > WIFI_CONFIG_MAX_DISABLE_DURATION_MILLIS) {
1022                     disableTimeoutMs = WIFI_CONFIG_MAX_DISABLE_DURATION_MILLIS;
1023                     break;
1024                 }
1025             }
1026             if (timeDifferenceMs >= disableTimeoutMs) {
1027                 return true;
1028             }
1029         }
1030         return false;
1031     }
1032 
1033     /**
1034      * Update a network's status (both internal and public) according to the update reason and
1035      * its current state. This method is expects to directly modify the internal WifiConfiguration
1036      * that is stored by WifiConfigManager.
1037      *
1038      * @param config the internal WifiConfiguration to be updated.
1039      * @param reason reason code for update.
1040      * @return true if the input configuration has been updated, false otherwise.
1041      */
updateNetworkSelectionStatus(WifiConfiguration config, int reason)1042     public boolean updateNetworkSelectionStatus(WifiConfiguration config, int reason) {
1043         if (reason < 0 || reason >= NetworkSelectionStatus.NETWORK_SELECTION_DISABLED_MAX) {
1044             Log.e(TAG, "Invalid Network disable reason " + reason);
1045             return false;
1046         }
1047         NetworkSelectionStatus networkStatus = config.getNetworkSelectionStatus();
1048         if (reason != NetworkSelectionStatus.DISABLED_NONE) {
1049             // Do not disable if in the exception list
1050             if (reason != NetworkSelectionStatus.DISABLED_BY_WIFI_MANAGER
1051                     && isConfigExemptFromBlocklist(config)) {
1052                 return false;
1053             }
1054 
1055             // Do not update SSID blocklist with information if this is the only
1056             // SSID be observed. By ignoring it we will cause additional failures
1057             // which will trigger Watchdog.
1058             if (reason == NetworkSelectionStatus.DISABLED_ASSOCIATION_REJECTION
1059                     || reason == NetworkSelectionStatus.DISABLED_AUTHENTICATION_FAILURE
1060                     || reason == NetworkSelectionStatus.DISABLED_DHCP_FAILURE) {
1061                 if (mWifiLastResortWatchdog.shouldIgnoreSsidUpdate()) {
1062                     if (mVerboseLoggingEnabled) {
1063                         Log.v(TAG, "Ignore update network selection status "
1064                                 + "since Watchdog trigger is activated");
1065                     }
1066                     return false;
1067                 }
1068             }
1069 
1070             networkStatus.incrementDisableReasonCounter(reason);
1071             // For network disable reasons, we should only update the status if we cross the
1072             // threshold.
1073             int disableReasonCounter = networkStatus.getDisableReasonCounter(reason);
1074             int disableReasonThreshold = getNetworkSelectionDisableThreshold(reason);
1075             if (disableReasonCounter < disableReasonThreshold) {
1076                 if (mVerboseLoggingEnabled) {
1077                     Log.v(TAG, "Disable counter for network " + config.getPrintableSsid()
1078                             + " for reason "
1079                             + NetworkSelectionStatus.getNetworkSelectionDisableReasonString(reason)
1080                             + " is " + networkStatus.getDisableReasonCounter(reason)
1081                             + " and threshold is " + disableReasonThreshold);
1082                 }
1083                 return true;
1084             }
1085         }
1086         setNetworkSelectionStatus(config, reason);
1087         return true;
1088     }
1089 
1090     /**
1091      * Sets a network's status (both internal and public) according to the update reason and
1092      * its current state.
1093      *
1094      * This updates the network's {@link WifiConfiguration#mNetworkSelectionStatus} field and the
1095      * public {@link WifiConfiguration#status} field if the network is either enabled or
1096      * permanently disabled.
1097      *
1098      * @param config network to be updated.
1099      * @param reason reason code for update.
1100      */
setNetworkSelectionStatus(WifiConfiguration config, int reason)1101     private void setNetworkSelectionStatus(WifiConfiguration config, int reason) {
1102         NetworkSelectionStatus networkStatus = config.getNetworkSelectionStatus();
1103         if (reason == NetworkSelectionStatus.DISABLED_NONE) {
1104             setNetworkSelectionEnabled(config);
1105         } else if (getNetworkSelectionDisableTimeoutMillis(reason)
1106                 != DisableReasonInfo.PERMANENT_DISABLE_TIMEOUT) {
1107             setNetworkSelectionTemporarilyDisabled(config, reason);
1108         } else {
1109             setNetworkSelectionPermanentlyDisabled(config, reason);
1110         }
1111         localLog("setNetworkSelectionStatus: configKey=" + config.getProfileKey()
1112                 + " networkStatus=" + networkStatus.getNetworkStatusString() + " disableReason="
1113                 + networkStatus.getNetworkSelectionDisableReasonString());
1114     }
1115 
1116     /**
1117      * Helper method to mark a network enabled for network selection.
1118      */
setNetworkSelectionEnabled(WifiConfiguration config)1119     private void setNetworkSelectionEnabled(WifiConfiguration config) {
1120         NetworkSelectionStatus status = config.getNetworkSelectionStatus();
1121         if (status.getNetworkSelectionStatus()
1122                 != NetworkSelectionStatus.NETWORK_SELECTION_ENABLED) {
1123             localLog("setNetworkSelectionEnabled: configKey=" + config.getProfileKey()
1124                     + " old networkStatus=" + status.getNetworkStatusString()
1125                     + " disableReason=" + status.getNetworkSelectionDisableReasonString());
1126         }
1127         status.setNetworkSelectionStatus(
1128                 NetworkSelectionStatus.NETWORK_SELECTION_ENABLED);
1129         status.setDisableTime(
1130                 NetworkSelectionStatus.INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP);
1131         status.setNetworkSelectionDisableReason(NetworkSelectionStatus.DISABLED_NONE);
1132 
1133         // Clear out all the disable reason counters.
1134         status.clearDisableReasonCounter();
1135         if (config.status == WifiConfiguration.Status.DISABLED) {
1136             config.status = WifiConfiguration.Status.ENABLED;
1137         }
1138     }
1139 
1140     /**
1141      * Helper method to mark a network temporarily disabled for network selection.
1142      */
setNetworkSelectionTemporarilyDisabled( WifiConfiguration config, int disableReason)1143     private void setNetworkSelectionTemporarilyDisabled(
1144             WifiConfiguration config, int disableReason) {
1145         NetworkSelectionStatus status = config.getNetworkSelectionStatus();
1146         status.setNetworkSelectionStatus(
1147                 NetworkSelectionStatus.NETWORK_SELECTION_TEMPORARY_DISABLED);
1148         // Only need a valid time filled in for temporarily disabled networks.
1149         status.setDisableTime(mClock.getElapsedSinceBootMillis());
1150         status.setNetworkSelectionDisableReason(disableReason);
1151         handleWifiConfigurationDisabled(config.SSID);
1152         mWifiMetrics.incrementWificonfigurationBlocklistCount(disableReason);
1153     }
1154 
1155     /**
1156      * Helper method to mark a network permanently disabled for network selection.
1157      */
setNetworkSelectionPermanentlyDisabled( WifiConfiguration config, int disableReason)1158     private void setNetworkSelectionPermanentlyDisabled(
1159             WifiConfiguration config, int disableReason) {
1160         NetworkSelectionStatus status = config.getNetworkSelectionStatus();
1161         status.setNetworkSelectionStatus(
1162                 NetworkSelectionStatus.NETWORK_SELECTION_PERMANENTLY_DISABLED);
1163         status.setDisableTime(
1164                 NetworkSelectionStatus.INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP);
1165         status.setNetworkSelectionDisableReason(disableReason);
1166         handleWifiConfigurationDisabled(config.SSID);
1167         config.status = WifiConfiguration.Status.DISABLED;
1168         mWifiMetrics.incrementWificonfigurationBlocklistCount(disableReason);
1169     }
1170 
1171     /**
1172      * Network Selection disable reason thresholds. These numbers are used to debounce network
1173      * failures before we disable them.
1174      *
1175      * @param reason int reason code
1176      * @return the disable threshold, or -1 if not found.
1177      */
1178     @VisibleForTesting
getNetworkSelectionDisableThreshold(@etworkSelectionDisableReason int reason)1179     public int getNetworkSelectionDisableThreshold(@NetworkSelectionDisableReason int reason) {
1180         DisableReasonInfo info = mDisableReasonInfo.get(reason);
1181         if (info == null) {
1182             Log.e(TAG, "Unrecognized network disable reason code for disable threshold: " + reason);
1183             return -1;
1184         } else {
1185             return info.mDisableThreshold;
1186         }
1187     }
1188 
1189     /**
1190      * Network Selection disable timeout for each kind of error. After the timeout in milliseconds,
1191      * enable the network again.
1192      */
1193     @VisibleForTesting
getNetworkSelectionDisableTimeoutMillis(@etworkSelectionDisableReason int reason)1194     public int getNetworkSelectionDisableTimeoutMillis(@NetworkSelectionDisableReason int reason) {
1195         DisableReasonInfo info = mDisableReasonInfo.get(reason);
1196         if (info == null) {
1197             Log.e(TAG, "Unrecognized network disable reason code for disable timeout: " + reason);
1198             return -1;
1199         } else {
1200             return info.mDisableTimeoutMillis;
1201         }
1202     }
1203 
1204     /**
1205      * Sets the allowlist ssids for the given ssid
1206      */
setAllowlistSsids(@onNull String ssid, @NonNull List<String> ssidAllowlist)1207     public void setAllowlistSsids(@NonNull String ssid, @NonNull List<String> ssidAllowlist) {
1208         mSsidAllowlistMap.put(ssid, ssidAllowlist);
1209     }
1210 }
1211