1 /*
2  * Copyright (C) 2021 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 android.net.wifi.util;
18 
19 import static android.net.wifi.ScanResult.FLAG_PASSPOINT_NETWORK;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.net.MacAddress;
24 import android.net.wifi.ScanResult;
25 import android.net.wifi.SecurityParams;
26 import android.net.wifi.WifiConfiguration;
27 import android.util.Log;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 
31 import java.io.PrintWriter;
32 import java.util.ArrayList;
33 import java.util.List;
34 /**
35  * Scan result utility for any {@link ScanResult} related operations.
36  * Currently contains:
37  *   > Helper methods to identify the encryption of a ScanResult.
38  * @hide
39  */
40 public class ScanResultUtil {
41     private static final String TAG = "ScanResultUtil";
ScanResultUtil()42     private ScanResultUtil() { /* not constructable */ }
43 
44     /**
45      * Helper method to check if the provided |scanResult| corresponds to a PSK network or not.
46      * This checks if the provided capabilities string contains PSK encryption type or not.
47      */
isScanResultForPskNetwork(@onNull ScanResult scanResult)48     public static boolean isScanResultForPskNetwork(@NonNull ScanResult scanResult) {
49         return scanResult.capabilities.contains("PSK");
50     }
51 
52     /**
53      * Helper method to check if the provided |scanResult| corresponds to a WAPI-PSK network or not.
54      * This checks if the provided capabilities string contains PSK encryption type or not.
55      */
isScanResultForWapiPskNetwork(@onNull ScanResult scanResult)56     public static boolean isScanResultForWapiPskNetwork(@NonNull ScanResult scanResult) {
57         return scanResult.capabilities.contains("WAPI-PSK");
58     }
59 
60     /**
61      * Helper method to check if the provided |scanResult| corresponds to a WAPI-CERT
62      * network or not.
63      * This checks if the provided capabilities string contains PSK encryption type or not.
64      */
isScanResultForWapiCertNetwork(@onNull ScanResult scanResult)65     public static boolean isScanResultForWapiCertNetwork(@NonNull ScanResult scanResult) {
66         return scanResult.capabilities.contains("WAPI-CERT");
67     }
68 
isScanResultForPmfMandatoryNetwork(@onNull ScanResult scanResult)69     private static boolean isScanResultForPmfMandatoryNetwork(@NonNull ScanResult scanResult) {
70         return scanResult.capabilities.contains("[MFPR]");
71     }
72 
isScanResultForPmfCapableNetwork(@onNull ScanResult scanResult)73     private static boolean isScanResultForPmfCapableNetwork(@NonNull ScanResult scanResult) {
74         return scanResult.capabilities.contains("[MFPC]");
75     }
76 
77     /**
78      * Helper method to check if the provided |scanResult| corresponds to a Passpoint R1/R2 network
79      * or not. Passpoint R1/R2 requirements:
80      * - Enterprise network not suite B.
81      * - Interworking bit is set.
82      * - HotSpot Release presents.
83      */
isEapScanResultForPasspointR1R2Network(@onNull ScanResult scanResult)84     public static boolean isEapScanResultForPasspointR1R2Network(@NonNull ScanResult scanResult) {
85         return scanResult.isPasspointNetwork();
86     }
87 
88     /**
89      * Helper method to check if the provided |scanResult| corresponds to a Passpoint R3 network or
90      * not. Passpoint R3 requirements:
91      * - Enterprise network not suite B.
92      * - Interworking bit is set.
93      * - HotSpot Release presents.
94      * - PMF is mandatory.
95      */
isEapScanResultForPasspointR3Network(@onNull ScanResult scanResult)96     public static boolean isEapScanResultForPasspointR3Network(@NonNull ScanResult scanResult) {
97         if (!isScanResultForPmfMandatoryNetwork(scanResult)) return false;
98 
99         return scanResult.isPasspointNetwork();
100     }
101 
102     /**
103      * Helper method to check if the provided |scanResult| corresponds to
104      * a WPA3 Enterprise transition network or not.
105      *
106      * See Section 3.3 WPA3-Enterprise transition mode in WPA3 Specification
107      * - Enable at least EAP/SHA1 and EAP/SHA256 AKM suites.
108      * - Not enable WPA1 version 1, WEP, and TKIP.
109      * - Management Frame Protection Capable is set.
110      * - Management Frame Protection Required is not set.
111      */
isScanResultForWpa3EnterpriseTransitionNetwork( @onNull ScanResult scanResult)112     public static boolean isScanResultForWpa3EnterpriseTransitionNetwork(
113             @NonNull ScanResult scanResult) {
114         return scanResult.capabilities.contains("EAP/SHA1")
115                 && scanResult.capabilities.contains("EAP/SHA256")
116                 && scanResult.capabilities.contains("RSN")
117                 && !scanResult.capabilities.contains("WEP")
118                 && !scanResult.capabilities.contains("TKIP")
119                 && !isScanResultForPmfMandatoryNetwork(scanResult)
120                 && isScanResultForPmfCapableNetwork(scanResult);
121     }
122 
123     /**
124      * Helper method to check if the provided |scanResult| corresponds to
125      * a WPA3 Enterprise only network or not.
126      *
127      * See Section 3.2 WPA3-Enterprise only mode in WPA3 Specification
128      * - Enable at least EAP/SHA256 AKM suite.
129      * - Not enable EAP/SHA1 AKM suite.
130      * - Not enable WPA1 version 1, WEP, and TKIP.
131      * - Management Frame Protection Capable is set.
132      * - Management Frame Protection Required is set.
133      */
isScanResultForWpa3EnterpriseOnlyNetwork(@onNull ScanResult scanResult)134     public static boolean isScanResultForWpa3EnterpriseOnlyNetwork(@NonNull ScanResult scanResult) {
135         return scanResult.capabilities.contains("EAP/SHA256")
136                 && !scanResult.capabilities.contains("EAP/SHA1")
137                 && scanResult.capabilities.contains("RSN")
138                 && !scanResult.capabilities.contains("WEP")
139                 && !scanResult.capabilities.contains("TKIP")
140                 && isScanResultForPmfMandatoryNetwork(scanResult)
141                 && isScanResultForPmfCapableNetwork(scanResult);
142     }
143 
144     /**
145      * Helper method to check if the provided |scanResult| corresponds to a WPA3-Enterprise 192-bit
146      * mode network or not.
147      * This checks if the provided capabilities comply these conditions:
148      * - Enable SUITE-B-192 AKM.
149      * - Not enable EAP/SHA1 AKM suite.
150      * - Not enable WPA1 version 1, WEP, and TKIP.
151      * - Management Frame Protection Required is set.
152      */
isScanResultForEapSuiteBNetwork(@onNull ScanResult scanResult)153     public static boolean isScanResultForEapSuiteBNetwork(@NonNull ScanResult scanResult) {
154         return scanResult.capabilities.contains("SUITE_B_192")
155                 && scanResult.capabilities.contains("RSN")
156                 && !scanResult.capabilities.contains("WEP")
157                 && !scanResult.capabilities.contains("TKIP")
158                 && isScanResultForPmfMandatoryNetwork(scanResult);
159     }
160 
161     /**
162      * Helper method to check if the provided |scanResult| corresponds to a WEP network or not.
163      * This checks if the provided capabilities string contains WEP encryption type or not.
164      */
isScanResultForWepNetwork(@onNull ScanResult scanResult)165     public static boolean isScanResultForWepNetwork(@NonNull ScanResult scanResult) {
166         return scanResult.capabilities.contains("WEP");
167     }
168 
169     /**
170      * Helper method to check if the provided |scanResult| corresponds to OWE network.
171      * This checks if the provided capabilities string contains OWE or not.
172      */
isScanResultForOweNetwork(@onNull ScanResult scanResult)173     public static boolean isScanResultForOweNetwork(@NonNull ScanResult scanResult) {
174         return scanResult.capabilities.contains("OWE");
175     }
176 
177     /**
178      * Helper method to check if the provided |scanResult| corresponds to OWE transition network.
179      * This checks if the provided capabilities string contains OWE_TRANSITION or not.
180      */
isScanResultForOweTransitionNetwork(@onNull ScanResult scanResult)181     public static boolean isScanResultForOweTransitionNetwork(@NonNull ScanResult scanResult) {
182         return scanResult.capabilities.contains("OWE_TRANSITION");
183     }
184 
185     /**
186      * Helper method to check if the provided |scanResult| corresponds to SAE network.
187      * This checks if the provided capabilities string contains SAE or not.
188      */
isScanResultForSaeNetwork(@onNull ScanResult scanResult)189     public static boolean isScanResultForSaeNetwork(@NonNull ScanResult scanResult) {
190         return scanResult.capabilities.contains("SAE");
191     }
192 
193     /**
194      * Helper method to check if the provided |scanResult| corresponds to PSK-SAE transition
195      * network. This checks if the provided capabilities string contains both PSK and SAE or not.
196      */
isScanResultForPskSaeTransitionNetwork(@onNull ScanResult scanResult)197     public static boolean isScanResultForPskSaeTransitionNetwork(@NonNull ScanResult scanResult) {
198         return scanResult.capabilities.contains("PSK") && scanResult.capabilities.contains("SAE");
199     }
200 
201     /**
202      * Helper method to check if the provided |scanResult| corresponds to FILS SHA256 network.
203      * This checks if the provided capabilities string contains FILS-SHA256 or not.
204      */
isScanResultForFilsSha256Network(@onNull ScanResult scanResult)205     public static boolean isScanResultForFilsSha256Network(@NonNull ScanResult scanResult) {
206         return scanResult.capabilities.contains("FILS-SHA256");
207     }
208 
209     /**
210      * Helper method to check if the provided |scanResult| corresponds to FILS SHA384 network.
211      * This checks if the provided capabilities string contains FILS-SHA384 or not.
212      */
isScanResultForFilsSha384Network(@onNull ScanResult scanResult)213     public static boolean isScanResultForFilsSha384Network(@NonNull ScanResult scanResult) {
214         return scanResult.capabilities.contains("FILS-SHA384");
215     }
216 
217     /**
218      * Helper method to check if the provided |scanResult| corresponds to DPP network.
219      * This checks if the provided capabilities string contains DPP or not.
220      */
isScanResultForDppNetwork(@onNull ScanResult scanResult)221     public static boolean isScanResultForDppNetwork(@NonNull ScanResult scanResult) {
222         return scanResult.capabilities.contains("DPP");
223     }
224 
225     /**
226      * Helper method to check if the provided |scanResult| corresponds to only WPA-Personal network.
227      * This checks if the provided capabilities string contains WPA and not RSN.
228      */
isScanResultForWpaPersonalOnlyNetwork(@onNull ScanResult scanResult)229     public static boolean isScanResultForWpaPersonalOnlyNetwork(@NonNull ScanResult scanResult) {
230         return isScanResultForPskNetwork(scanResult) && !scanResult.capabilities.contains("RSN");
231     }
232 
233     /**
234      *  Helper method to check if the provided |scanResult| corresponds to an unknown amk network.
235      *  This checks if the provided capabilities string contains ? or not.
236      */
isScanResultForUnknownAkmNetwork(@onNull ScanResult scanResult)237     public static boolean isScanResultForUnknownAkmNetwork(@NonNull ScanResult scanResult) {
238         return scanResult.capabilities.contains("?");
239     }
240 
241     /**
242      *  Helper method to check if the provided |scanResult| corresponds to a pure PSK network.
243      */
isScanResultForPskOnlyNetwork(@onNull ScanResult r)244     public static boolean isScanResultForPskOnlyNetwork(@NonNull ScanResult r) {
245         return ScanResultUtil.isScanResultForPskNetwork(r)
246                 && !ScanResultUtil.isScanResultForSaeNetwork(r);
247     }
248 
249     /**
250      *  Helper method to check if the provided |scanResult| corresponds to a pure SAE network.
251      */
isScanResultForSaeOnlyNetwork(@onNull ScanResult r)252     public static boolean isScanResultForSaeOnlyNetwork(@NonNull ScanResult r) {
253         return !ScanResultUtil.isScanResultForPskNetwork(r)
254                 && ScanResultUtil.isScanResultForSaeNetwork(r);
255     }
256 
257     /**
258      *  Helper method to check if the provided |scanResult| corresponds to a pure OPEN network.
259      */
isScanResultForOpenOnlyNetwork(@onNull ScanResult r)260     public static boolean isScanResultForOpenOnlyNetwork(@NonNull ScanResult r) {
261         return ScanResultUtil.isScanResultForOpenNetwork(r)
262                 && !ScanResultUtil.isScanResultForOweNetwork(r);
263     }
264 
265     /**
266      *  Helper method to check if the provided |scanResult| corresponds to a pure OWE network.
267      */
isScanResultForOweOnlyNetwork(@onNull ScanResult r)268     public static boolean isScanResultForOweOnlyNetwork(@NonNull ScanResult r) {
269         return !ScanResultUtil.isScanResultForOweTransitionNetwork(r)
270                 && ScanResultUtil.isScanResultForOweNetwork(r);
271     }
272 
273     /**
274      * Helper method to check if the provided |scanResult| corresponds to a pure WPA2 Enterprise
275      * network.
276      */
isScanResultForWpa2EnterpriseOnlyNetwork(@onNull ScanResult scanResult)277     public static boolean isScanResultForWpa2EnterpriseOnlyNetwork(@NonNull ScanResult scanResult) {
278         return (scanResult.capabilities.contains("EAP/SHA1")
279                         || scanResult.capabilities.contains("EAP/SHA256")
280                         || scanResult.capabilities.contains("FT/EAP")
281                         || scanResult.capabilities.contains("EAP-FILS"))
282                 && !isScanResultForWpa3EnterpriseOnlyNetwork(scanResult)
283                 && !isScanResultForWpa3EnterpriseTransitionNetwork(scanResult);
284     }
285 
286     /**
287      * Helper method to check if the provided |scanResult| corresponds to an open network or not.
288      * This checks if the provided capabilities string does not contain either of WEP, PSK, SAE
289      * EAP, or unknown encryption types or not.
290      */
isScanResultForOpenNetwork(@onNull ScanResult scanResult)291     public static boolean isScanResultForOpenNetwork(@NonNull ScanResult scanResult) {
292         return (!(isScanResultForWepNetwork(scanResult)
293                 || isScanResultForPskNetwork(scanResult)
294                 || isScanResultForWpa2EnterpriseOnlyNetwork(scanResult)
295                 || isScanResultForSaeNetwork(scanResult)
296                 || isScanResultForWpa3EnterpriseTransitionNetwork(scanResult)
297                 || isScanResultForWpa3EnterpriseOnlyNetwork(scanResult)
298                 || isScanResultForWapiPskNetwork(scanResult)
299                 || isScanResultForWapiCertNetwork(scanResult)
300                 || isScanResultForEapSuiteBNetwork(scanResult)
301                 || isScanResultForDppNetwork(scanResult)
302                 || isScanResultForUnknownAkmNetwork(scanResult)));
303     }
304 
305     /**
306      * Helper method to quote the SSID in Scan result to use for comparing/filling SSID stored in
307      * WifiConfiguration object.
308      */
309     @VisibleForTesting
createQuotedSsid(@ullable String ssid)310     public static @NonNull String createQuotedSsid(@Nullable String ssid) {
311         return "\"" + ssid + "\"";
312     }
313 
314     /**
315      * Creates a network configuration object using the provided |scanResult|.
316      */
createNetworkFromScanResult( @onNull ScanResult scanResult)317     public static @Nullable WifiConfiguration createNetworkFromScanResult(
318             @NonNull ScanResult scanResult) {
319         WifiConfiguration config = new WifiConfiguration();
320         config.SSID = createQuotedSsid(scanResult.SSID);
321         List<SecurityParams> list = generateSecurityParamsListFromScanResult(scanResult);
322         if (list.isEmpty()) {
323             return null;
324         }
325         config.setSecurityParams(list);
326         return config;
327     }
328 
329     /**
330      * Generate security params from the scan result.
331      * @param scanResult the scan result to be checked.
332      * @return a list of security params. If no known security params, return an empty list.
333      */
generateSecurityParamsListFromScanResult( @onNull ScanResult scanResult)334     public static @NonNull List<SecurityParams> generateSecurityParamsListFromScanResult(
335             @NonNull ScanResult scanResult) {
336         List<SecurityParams> list = new ArrayList<>();
337 
338         // Open network & its upgradable types
339         if (ScanResultUtil.isScanResultForOweTransitionNetwork(scanResult)) {
340             list.add(SecurityParams.createSecurityParamsBySecurityType(
341                     WifiConfiguration.SECURITY_TYPE_OPEN));
342             list.add(SecurityParams.createSecurityParamsBySecurityType(
343                     WifiConfiguration.SECURITY_TYPE_OWE));
344             return list;
345         } else if (ScanResultUtil.isScanResultForOweNetwork(scanResult)) {
346             list.add(SecurityParams.createSecurityParamsBySecurityType(
347                     WifiConfiguration.SECURITY_TYPE_OWE));
348             return list;
349         } else if (ScanResultUtil.isScanResultForOpenNetwork(scanResult)) {
350             list.add(SecurityParams.createSecurityParamsBySecurityType(
351                     WifiConfiguration.SECURITY_TYPE_OPEN));
352             return list;
353         }
354 
355         // WEP network which has no upgradable type
356         if (ScanResultUtil.isScanResultForWepNetwork(scanResult)) {
357             list.add(SecurityParams.createSecurityParamsBySecurityType(
358                     WifiConfiguration.SECURITY_TYPE_WEP));
359             return list;
360         }
361 
362         // WAPI PSK network which has no upgradable type
363         if (ScanResultUtil.isScanResultForWapiPskNetwork(scanResult)) {
364             list.add(SecurityParams.createSecurityParamsBySecurityType(
365                     WifiConfiguration.SECURITY_TYPE_WAPI_PSK));
366             return list;
367         }
368 
369         // WAPI CERT network which has no upgradable type
370         if (ScanResultUtil.isScanResultForWapiCertNetwork(scanResult)) {
371             list.add(SecurityParams.createSecurityParamsBySecurityType(
372                     WifiConfiguration.SECURITY_TYPE_WAPI_CERT));
373             return list;
374         }
375 
376         // WPA2 personal network & its upgradable types
377         if (ScanResultUtil.isScanResultForPskNetwork(scanResult)
378                 && ScanResultUtil.isScanResultForSaeNetwork(scanResult)) {
379             list.add(SecurityParams.createSecurityParamsBySecurityType(
380                     WifiConfiguration.SECURITY_TYPE_PSK));
381             list.add(SecurityParams.createSecurityParamsBySecurityType(
382                     WifiConfiguration.SECURITY_TYPE_SAE));
383             return list;
384         } else if (ScanResultUtil.isScanResultForPskNetwork(scanResult)) {
385             list.add(SecurityParams.createSecurityParamsBySecurityType(
386                     WifiConfiguration.SECURITY_TYPE_PSK));
387             return list;
388         } else if (ScanResultUtil.isScanResultForSaeNetwork(scanResult)) {
389             list.add(SecurityParams.createSecurityParamsBySecurityType(
390                     WifiConfiguration.SECURITY_TYPE_SAE));
391             return list;
392         } else if (ScanResultUtil.isScanResultForDppNetwork(scanResult)) {
393             list.add(SecurityParams.createSecurityParamsBySecurityType(
394                     WifiConfiguration.SECURITY_TYPE_DPP));
395             return list;
396         }
397 
398         boolean isEapNetworkAndNotSuiteB = false;
399         // WPA3 Enterprise 192-bit mode, WPA2/WPA3 enterprise network & its upgradable types
400         if (ScanResultUtil.isScanResultForEapSuiteBNetwork(scanResult)) {
401             list.add(SecurityParams.createSecurityParamsBySecurityType(
402                     WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT));
403         } else if (ScanResultUtil.isScanResultForWpa3EnterpriseTransitionNetwork(scanResult)) {
404             list.add(SecurityParams.createSecurityParamsBySecurityType(
405                     WifiConfiguration.SECURITY_TYPE_EAP));
406             list.add(SecurityParams.createSecurityParamsBySecurityType(
407                     WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE));
408             isEapNetworkAndNotSuiteB = true;
409         } else if (ScanResultUtil.isScanResultForWpa3EnterpriseOnlyNetwork(scanResult)) {
410             list.add(SecurityParams.createSecurityParamsBySecurityType(
411                     WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE));
412             isEapNetworkAndNotSuiteB = true;
413         } else if (ScanResultUtil.isScanResultForWpa2EnterpriseOnlyNetwork(scanResult)) {
414             list.add(SecurityParams.createSecurityParamsBySecurityType(
415                     WifiConfiguration.SECURITY_TYPE_EAP));
416             isEapNetworkAndNotSuiteB = true;
417         }
418         if (!isEapNetworkAndNotSuiteB) {
419             return list;
420         }
421         // An Enterprise network might be a Passpoint network as well.
422         // R3 network might be also a valid R1/R2 network.
423         if (isEapScanResultForPasspointR1R2Network(scanResult)) {
424             list.add(SecurityParams.createSecurityParamsBySecurityType(
425                     WifiConfiguration.SECURITY_TYPE_PASSPOINT_R1_R2));
426         }
427         if (isEapScanResultForPasspointR3Network(scanResult)) {
428             list.add(SecurityParams.createSecurityParamsBySecurityType(
429                     WifiConfiguration.SECURITY_TYPE_PASSPOINT_R3));
430         }
431         return list;
432     }
433 
434     /**
435      * Dump the provided scan results list to |pw|.
436      */
dumpScanResults(@onNull PrintWriter pw, @Nullable List<ScanResult> scanResults, long nowMs)437     public static void dumpScanResults(@NonNull PrintWriter pw,
438             @Nullable List<ScanResult> scanResults, long nowMs) {
439         if (scanResults != null && scanResults.size() != 0) {
440             pw.println("    BSSID              Frequency      RSSI           Age(sec)     SSID "
441                     + "                                Flags");
442             for (ScanResult r : scanResults) {
443                 long timeStampMs = r.timestamp / 1000;
444                 String age;
445                 if (timeStampMs <= 0) {
446                     age = "___?___";
447                 } else if (nowMs < timeStampMs) {
448                     age = "  0.000";
449                 } else if (timeStampMs < nowMs - 1000000) {
450                     age = ">1000.0";
451                 } else {
452                     age = String.format("%3.3f", (nowMs - timeStampMs) / 1000.0);
453                 }
454                 String ssid = r.SSID == null ? "" : r.SSID;
455                 String rssiInfo = "";
456                 int numRadioChainInfos = r.radioChainInfos == null ? 0 : r.radioChainInfos.length;
457                 if (numRadioChainInfos == 1) {
458                     rssiInfo = String.format("%5d(%1d:%3d)       ", r.level,
459                             r.radioChainInfos[0].id, r.radioChainInfos[0].level);
460                 } else if (numRadioChainInfos == 2) {
461                     rssiInfo = String.format("%5d(%1d:%3d/%1d:%3d)", r.level,
462                             r.radioChainInfos[0].id, r.radioChainInfos[0].level,
463                             r.radioChainInfos[1].id, r.radioChainInfos[1].level);
464                 } else {
465                     rssiInfo = String.format("%9d         ", r.level);
466                 }
467                 if ((r.flags & FLAG_PASSPOINT_NETWORK)
468                         == FLAG_PASSPOINT_NETWORK) {
469                     r.capabilities += "[PASSPOINT]";
470                 }
471                 pw.printf("  %17s  %9d  %18s   %7s    %-32s  %s\n",
472                         r.BSSID,
473                         r.frequency,
474                         rssiInfo,
475                         age,
476                         String.format("%1.32s", ssid),
477                         r.capabilities);
478             }
479         }
480     }
481 
482     /**
483      * Check if ScanResult list is valid.
484      */
validateScanResultList(@ullable List<ScanResult> scanResults)485     public static boolean validateScanResultList(@Nullable List<ScanResult> scanResults) {
486         if (scanResults == null || scanResults.isEmpty()) {
487             Log.w(TAG, "Empty or null ScanResult list");
488             return false;
489         }
490         for (ScanResult scanResult : scanResults) {
491             if (!validate(scanResult)) {
492                 Log.w(TAG, "Invalid ScanResult: " + scanResult);
493                 return false;
494             }
495         }
496         return true;
497     }
498 
validate(@ullable ScanResult scanResult)499     private static boolean validate(@Nullable ScanResult scanResult) {
500         return scanResult != null && scanResult.SSID != null
501                 && scanResult.capabilities != null && scanResult.BSSID != null;
502     }
503 
504     /**
505      * Redact bytes from a bssid.
506      */
redactBssid(MacAddress bssid, int numRedactedOctets)507     public static String redactBssid(MacAddress bssid, int numRedactedOctets) {
508         if (bssid == null) {
509             return "";
510         }
511         StringBuilder redactedBssid = new StringBuilder();
512         byte[] bssidBytes = bssid.toByteArray();
513 
514         if (numRedactedOctets < 0 || numRedactedOctets > 6) {
515             // Reset to default if passed value is invalid.
516             numRedactedOctets = 4;
517         }
518         for (int i = 0; i < 6; i++) {
519             if (i < numRedactedOctets) {
520                 redactedBssid.append("xx");
521             } else {
522                 redactedBssid.append(String.format("%02X", bssidBytes[i]));
523             }
524             if (i != 5) {
525                 redactedBssid.append(":");
526             }
527         }
528         return redactedBssid.toString();
529     }
530 }
531