Index: chrome/android/java/src/org/chromium/chrome/browser/omnibox/geo/GeolocationHeader.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/geo/GeolocationHeader.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/geo/GeolocationHeader.java |
index 6691246d77dd159b3d002b38c374c9fa5fc29bdf..c409fba07aa861d95a74292b90ff18c2825f04f8 100644 |
--- a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/geo/GeolocationHeader.java |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/geo/GeolocationHeader.java |
@@ -19,13 +19,17 @@ import android.util.Base64; |
import com.google.protobuf.nano.MessageNano; |
import org.chromium.base.ApiCompatibilityUtils; |
+import org.chromium.base.CollectionUtil; |
import org.chromium.base.ContextUtils; |
import org.chromium.base.Log; |
+import org.chromium.base.VisibleForTesting; |
import org.chromium.base.annotations.CalledByNative; |
import org.chromium.base.annotations.SuppressFBWarnings; |
import org.chromium.base.metrics.RecordHistogram; |
import org.chromium.chrome.browser.ChromeFeatureList; |
import org.chromium.chrome.browser.UrlConstants; |
+import org.chromium.chrome.browser.omnibox.geo.VisibleNetworks.VisibleCell; |
+import org.chromium.chrome.browser.omnibox.geo.VisibleNetworks.VisibleWifi; |
import org.chromium.chrome.browser.preferences.website.ContentSetting; |
import org.chromium.chrome.browser.preferences.website.GeolocationInfo; |
import org.chromium.chrome.browser.preferences.website.WebsitePreferenceBridge; |
@@ -35,8 +39,11 @@ import org.chromium.chrome.browser.util.UrlUtilities; |
import java.lang.annotation.Retention; |
import java.lang.annotation.RetentionPolicy; |
import java.util.Locale; |
+import java.util.Set; |
import java.util.concurrent.TimeUnit; |
+import javax.annotation.Nullable; |
+ |
/** |
* Provides methods for building the X-Geo HTTP header, which provides device location to a server |
* when making an HTTP request. |
@@ -148,10 +155,14 @@ public class GeolocationHeader { |
UMA_PERM_NOT_HTTPS}) |
public @interface UmaPermission {} |
- private static final int LOCATION_SOURCE_HIGH_ACCURACY = 0; |
- private static final int LOCATION_SOURCE_BATTERY_SAVING = 1; |
- private static final int LOCATION_SOURCE_GPS_ONLY = 2; |
- private static final int LOCATION_SOURCE_MASTER_OFF = 3; |
+ @VisibleForTesting |
+ static final int LOCATION_SOURCE_HIGH_ACCURACY = 0; |
+ @VisibleForTesting |
+ static final int LOCATION_SOURCE_BATTERY_SAVING = 1; |
+ @VisibleForTesting |
+ static final int LOCATION_SOURCE_GPS_ONLY = 2; |
+ @VisibleForTesting |
+ static final int LOCATION_SOURCE_MASTER_OFF = 3; |
@Retention(RetentionPolicy.SOURCE) |
@IntDef({LOCATION_SOURCE_HIGH_ACCURACY, LOCATION_SOURCE_BATTERY_SAVING, |
LOCATION_SOURCE_GPS_ONLY, LOCATION_SOURCE_MASTER_OFF}) |
@@ -186,17 +197,35 @@ public class GeolocationHeader { |
private static final int REFRESH_LOCATION_AGE = 5 * 60 * 1000; // 5 minutes |
/** The X-Geo header prefix, preceding any location descriptors */ |
- private static final String XGEO_HEADER_PREFIX = "X-Geo: "; |
+ private static final String XGEO_HEADER_PREFIX = "X-Geo:"; |
+ |
+ /** |
+ * The location descriptor separator used in the X-Geo header to separate encoding prefix, and |
+ * encoded descriptors |
+ */ |
+ private static final String LOCATION_SEPARATOR = " "; |
/** The location descriptor prefix used in the X-Geo header to specify a proto wire encoding */ |
- private static final String LOCATION_PROTO_PREFIX = "w "; |
+ private static final String LOCATION_PROTO_PREFIX = "w"; |
/** The location descriptor prefix used in the X-Geo header to specify an ASCII encoding */ |
- private static final String LOCATION_ASCII_PREFIX = "a "; |
+ private static final String LOCATION_ASCII_PREFIX = "a"; |
/** The time of the first location refresh. Contains Long.MAX_VALUE if not set. */ |
private static long sFirstLocationTime = Long.MAX_VALUE; |
+ /** Present in WiFi SSID that should not be mapped */ |
+ private static final String SSID_NOMAP = "_nomap"; |
+ |
+ /** Present in WiFi SSID that opted out */ |
+ private static final String SSID_OPTOUT = "_optout"; |
+ |
+ private static int sLocationSourceForTesting; |
+ private static boolean sUseLocationSourceForTesting; |
+ |
+ private static boolean sAppPermissionGrantedForTesting; |
+ private static boolean sUseAppPermissionGrantedForTesting; |
+ |
/** |
* Requests a location refresh so that a valid location will be available for constructing |
* an X-Geo header in the near future (i.e. within 5 minutes). |
@@ -209,6 +238,11 @@ public class GeolocationHeader { |
} |
GeolocationTracker.refreshLastKnownLocation( |
ContextUtils.getApplicationContext(), REFRESH_LOCATION_AGE); |
+ |
+ // Only refresh visible networks if enabled. |
+ if (ChromeFeatureList.isEnabled(ChromeFeatureList.XGEO_VISIBLE_NETWORKS)) { |
+ VisibleNetworksTracker.refreshVisibleNetworks(ContextUtils.getApplicationContext()); |
+ } |
} |
/** |
@@ -260,31 +294,46 @@ public class GeolocationHeader { |
* @return The X-Geo header string or null. |
*/ |
public static String getGeoHeader(String url, Tab tab) { |
+ // TODO(lbargu): Refactor and simplify flow. |
boolean isIncognito = tab.isIncognito(); |
- boolean locationAttached = true; |
- Location location = null; |
+ Location locationToAttach = null; |
+ VisibleNetworks visibleNetworksToAttach = null; |
long locationAge = Long.MAX_VALUE; |
@HeaderState int headerState = geoHeaderStateForUrl(url, isIncognito, true); |
+ // XGEO_VISIBLE_NETWORKS |
+ // When this feature is enabled, we will send visible WiFi and Cell Access Points as part of |
+ // the X-GEO HTTP Header so that we can better position the client server side in the case |
+ // where there is no lat/long or it's too old. |
boolean isXGeoVisibleNetworksEnabled = |
ChromeFeatureList.isEnabled(ChromeFeatureList.XGEO_VISIBLE_NETWORKS); |
if (headerState == HEADER_ENABLED) { |
// Only send X-Geo header if there's a fresh location available. |
// Use flag controlling visible network changes to decide whether GPS location should be |
// included as a fallback. |
- location = GeolocationTracker.getLastKnownLocation( |
+ // TODO(lbargu): Measure timing here and to get visible networks. |
+ locationToAttach = GeolocationTracker.getLastKnownLocation( |
ContextUtils.getApplicationContext(), isXGeoVisibleNetworksEnabled); |
- if (location == null) { |
+ if (locationToAttach == null) { |
recordHistogram(UMA_LOCATION_NOT_AVAILABLE); |
- locationAttached = false; |
} else { |
- locationAge = GeolocationTracker.getLocationAge(location); |
+ locationAge = GeolocationTracker.getLocationAge(locationToAttach); |
if (locationAge > MAX_LOCATION_AGE) { |
+ // Do not attach the location |
recordHistogram(UMA_LOCATION_STALE); |
- locationAttached = false; |
+ locationToAttach = null; |
+ } else { |
+ recordHistogram(UMA_HEADER_SENT); |
} |
} |
- } else { |
- locationAttached = false; |
+ |
+ // The header state is enabled, so this means we have app permissions, and the url is |
+ // allowed to receive location. Before attempting to attach visible networks, check if |
+ // network-based location is enabled. |
+ if (isXGeoVisibleNetworksEnabled && isNetworkLocationEnabled() |
+ && !isLocationFresh(locationToAttach)) { |
+ visibleNetworksToAttach = VisibleNetworksTracker.getLastKnownVisibleNetworks( |
+ ContextUtils.getApplicationContext()); |
+ } |
} |
@LocationSource int locationSource = getLocationSource(); |
@@ -292,8 +341,8 @@ public class GeolocationHeader { |
@Permission int domainPermission = getDomainPermission(url, isIncognito); |
// Record the permission state with a histogram. |
- recordPermissionHistogram( |
- locationSource, appPermission, domainPermission, locationAttached, headerState); |
+ recordPermissionHistogram(locationSource, appPermission, domainPermission, |
+ locationToAttach != null, headerState); |
if (locationSource != LOCATION_SOURCE_MASTER_OFF && appPermission != PERMISSION_BLOCKED |
&& domainPermission != PERMISSION_BLOCKED && !isIncognito) { |
@@ -303,60 +352,38 @@ public class GeolocationHeader { |
? 0 |
: SystemClock.elapsedRealtime() - sFirstLocationTime; |
// Record the Time Listening with a histogram. |
- recordTimeListeningHistogram(locationSource, locationAttached, duration); |
+ recordTimeListeningHistogram(locationSource, locationToAttach != null, duration); |
} |
- // Note that strictly speaking "location == null" is not needed here as the |
- // logic above prevents location being null when locationAttached is true. |
- // It is here to prevent problems if the logic above is changed. |
- if (!locationAttached || location == null) return null; |
- |
- recordHistogram(UMA_HEADER_SENT); |
- |
- // Timestamp in microseconds since the UNIX epoch. |
- long timestamp = location.getTime() * 1000; |
- // Latitude times 1e7. |
- int latitudeE7 = (int) (location.getLatitude() * 10000000); |
- // Longitude times 1e7. |
- int longitudeE7 = (int) (location.getLongitude() * 10000000); |
- // Radius of 68% accuracy in mm. |
- int radius = (int) (location.getAccuracy() * 1000); |
- |
- // Encode location using ascii protobuf format followed by base64 encoding. |
- // https://goto.google.com/partner_location_proto |
- String locationAscii = String.format(Locale.US, |
- "role:1 producer:12 timestamp:%d latlng{latitude_e7:%d longitude_e7:%d} radius:%d", |
- timestamp, latitudeE7, longitudeE7, radius); |
- String locationAsciiEncoding = |
- new String(Base64.encode(locationAscii.getBytes(), Base64.NO_WRAP)); |
if (!isXGeoVisibleNetworksEnabled) { |
- return XGEO_HEADER_PREFIX + LOCATION_ASCII_PREFIX + locationAsciiEncoding; |
+ String locationAsciiEncoding = encodeAsciiLocation(locationToAttach); |
+ if (locationAsciiEncoding == null) return null; |
+ return XGEO_HEADER_PREFIX + LOCATION_SEPARATOR + LOCATION_ASCII_PREFIX |
+ + LOCATION_SEPARATOR + locationAsciiEncoding; |
} |
- // Create a LatLng for the coordinates. |
- PartnerLocationDescriptor.LatLng latlng = new PartnerLocationDescriptor.LatLng(); |
- latlng.latitudeE7 = latitudeE7; |
- latlng.longitudeE7 = longitudeE7; |
+ // Proto encoding |
+ String locationProtoEncoding = encodeProtoLocation(locationToAttach); |
+ String visibleNetworksProtoEncoding = encodeProtoVisibleNetworks(visibleNetworksToAttach); |
- // Populate a LocationDescriptor with the LatLng. |
- PartnerLocationDescriptor.LocationDescriptor locationDescriptor = |
- new PartnerLocationDescriptor.LocationDescriptor(); |
- locationDescriptor.latlng = latlng; |
- // Include role, producer, timestamp and radius. |
- locationDescriptor.role = PartnerLocationDescriptor.CURRENT_LOCATION; |
- locationDescriptor.producer = PartnerLocationDescriptor.DEVICE_LOCATION; |
- locationDescriptor.timestamp = timestamp; |
- locationDescriptor.radius = (float) radius; |
- |
- String locationProtoEncoding = Base64.encodeToString( |
- MessageNano.toByteArray(locationDescriptor), Base64.NO_WRAP | Base64.URL_SAFE); |
+ if (locationProtoEncoding == null && visibleNetworksProtoEncoding == null) return null; |
- return XGEO_HEADER_PREFIX + LOCATION_PROTO_PREFIX + locationProtoEncoding; |
+ StringBuilder header = new StringBuilder(XGEO_HEADER_PREFIX); |
+ if (locationProtoEncoding != null) { |
+ header.append(LOCATION_SEPARATOR).append(LOCATION_PROTO_PREFIX) |
+ .append(LOCATION_SEPARATOR).append(locationProtoEncoding); |
+ } |
+ if (visibleNetworksProtoEncoding != null) { |
+ header.append(LOCATION_SEPARATOR).append(LOCATION_PROTO_PREFIX) |
+ .append(LOCATION_SEPARATOR).append(visibleNetworksProtoEncoding); |
+ } |
+ return header.toString(); |
} |
@CalledByNative |
static boolean hasGeolocationPermission() { |
+ if (sUseAppPermissionGrantedForTesting) return sAppPermissionGrantedForTesting; |
int pid = Process.myPid(); |
int uid = Process.myUid(); |
if (ApiCompatibilityUtils.checkPermission(ContextUtils.getApplicationContext(), |
@@ -384,6 +411,9 @@ public class GeolocationHeader { |
*/ |
@Permission |
static int getGeolocationPermission(Tab tab) { |
+ if (sUseAppPermissionGrantedForTesting) { |
+ return sAppPermissionGrantedForTesting ? PERMISSION_GRANTED : PERMISSION_BLOCKED; |
+ } |
if (hasGeolocationPermission()) return PERMISSION_GRANTED; |
return tab.getWindowAndroid().canRequestPermission( |
Manifest.permission.ACCESS_COARSE_LOCATION) |
@@ -416,6 +446,18 @@ public class GeolocationHeader { |
return locationPermission; |
} |
+ @VisibleForTesting |
+ static void setLocationSourceForTesting(int locationSourceForTesting) { |
+ sLocationSourceForTesting = locationSourceForTesting; |
+ sUseLocationSourceForTesting = true; |
+ } |
+ |
+ @VisibleForTesting |
+ static void setAppPermissionGrantedForTesting(boolean appPermissionGrantedForTesting) { |
+ sAppPermissionGrantedForTesting = appPermissionGrantedForTesting; |
+ sUseAppPermissionGrantedForTesting = true; |
+ } |
+ |
/** Records a data point for the Geolocation.HeaderSentOrNot histogram. */ |
private static void recordHistogram(int result) { |
RecordHistogram.recordEnumeratedHistogram("Geolocation.HeaderSentOrNot", result, UMA_MAX); |
@@ -426,6 +468,8 @@ public class GeolocationHeader { |
// We should replace our usage of LOCATION_PROVIDERS_ALLOWED when the min API is 19. |
@SuppressWarnings("deprecation") |
private static int getLocationSource() { |
+ if (sUseLocationSourceForTesting) return sLocationSourceForTesting; |
+ |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { |
int locationMode; |
try { |
@@ -462,6 +506,17 @@ public class GeolocationHeader { |
} |
} |
+ private static boolean isNetworkLocationEnabled() { |
+ int locationSource = getLocationSource(); |
+ return locationSource == LOCATION_SOURCE_HIGH_ACCURACY |
+ || locationSource == LOCATION_SOURCE_BATTERY_SAVING; |
+ } |
+ |
+ private static boolean isLocationFresh(@Nullable Location location) { |
+ return location != null |
+ && GeolocationTracker.getLocationAge(location) <= REFRESH_LOCATION_AGE; |
+ } |
+ |
/** |
* Returns the domain permission as either granted, blocked or prompt. |
* This is based upon the location permission for sharing their location with url (e.g. via the |
@@ -675,4 +730,210 @@ public class GeolocationHeader { |
RecordHistogram.recordCustomCountHistogram( |
name, duration, 1, LOCATION_AGE_HISTOGRAM_MAX_SECONDS, 50); |
} |
+ |
+ /** |
+ * Encodes location into ascii encoding. |
+ */ |
+ @Nullable |
+ @VisibleForTesting |
+ static String encodeAsciiLocation(@Nullable Location location) { |
+ if (location == null) return null; |
+ |
+ // Timestamp in microseconds since the UNIX epoch. |
+ long timestamp = location.getTime() * 1000; |
+ // Latitude times 1e7. |
+ int latitudeE7 = (int) (location.getLatitude() * 10000000); |
+ // Longitude times 1e7. |
+ int longitudeE7 = (int) (location.getLongitude() * 10000000); |
+ // Radius of 68% accuracy in mm. |
+ int radius = (int) (location.getAccuracy() * 1000); |
+ |
+ // Encode location using ascii protobuf format followed by base64 encoding. |
+ // https://goto.google.com/partner_location_proto |
+ String locationAscii = String.format(Locale.US, |
+ "role:1 producer:12 timestamp:%d latlng{latitude_e7:%d longitude_e7:%d}" |
+ + " radius:%d", |
+ timestamp, latitudeE7, longitudeE7, radius); |
+ return new String(Base64.encode(locationAscii.getBytes(), Base64.NO_WRAP)); |
+ } |
+ |
+ /** |
+ * Encodes location into proto encoding. |
+ */ |
+ @Nullable |
+ @VisibleForTesting |
+ static String encodeProtoLocation(@Nullable Location location) { |
+ if (location == null) return null; |
+ |
+ // Timestamp in microseconds since the UNIX epoch. |
+ long timestamp = location.getTime() * 1000; |
+ // Latitude times 1e7. |
+ int latitudeE7 = (int) (location.getLatitude() * 10000000); |
+ // Longitude times 1e7. |
+ int longitudeE7 = (int) (location.getLongitude() * 10000000); |
+ // Radius of 68% accuracy in mm. |
+ int radius = (int) (location.getAccuracy() * 1000); |
+ |
+ // Create a LatLng for the coordinates. |
+ PartnerLocationDescriptor.LatLng latlng = new PartnerLocationDescriptor.LatLng(); |
+ latlng.latitudeE7 = latitudeE7; |
+ latlng.longitudeE7 = longitudeE7; |
+ |
+ // Populate a LocationDescriptor with the LatLng. |
+ PartnerLocationDescriptor.LocationDescriptor locationDescriptor = |
+ new PartnerLocationDescriptor.LocationDescriptor(); |
+ locationDescriptor.latlng = latlng; |
+ // Include role, producer, timestamp and radius. |
+ locationDescriptor.role = PartnerLocationDescriptor.CURRENT_LOCATION; |
+ locationDescriptor.producer = PartnerLocationDescriptor.DEVICE_LOCATION; |
+ locationDescriptor.timestamp = timestamp; |
+ locationDescriptor.radius = (float) radius; |
+ return encodeLocationDescriptor(locationDescriptor); |
+ } |
+ |
+ /** |
+ * Encodes the given proto location descriptor into a BASE64 URL_SAFE encoding. |
+ */ |
+ private static String encodeLocationDescriptor( |
+ PartnerLocationDescriptor.LocationDescriptor locationDescriptor) { |
+ return Base64.encodeToString( |
+ MessageNano.toByteArray(locationDescriptor), Base64.NO_WRAP | Base64.URL_SAFE); |
+ } |
+ |
+ /** |
+ * Encodes visible networks in proto encoding. |
+ */ |
+ @Nullable |
+ @VisibleForTesting |
+ static String encodeProtoVisibleNetworks(@Nullable VisibleNetworks visibleNetworks) { |
+ VisibleNetworks visibleNetworksToEncode = trimVisibleNetworks(visibleNetworks); |
+ if (visibleNetworksToEncode == null) { |
+ // No data to encode. |
+ return null; |
+ } |
+ VisibleWifi connectedWifi = visibleNetworksToEncode.connectedWifi(); |
+ VisibleCell connectedCell = visibleNetworksToEncode.connectedCell(); |
+ Set<VisibleWifi> visibleWifis = visibleNetworksToEncode.allVisibleWifis(); |
+ Set<VisibleCell> visibleCells = visibleNetworksToEncode.allVisibleCells(); |
+ |
+ int numVisibleNetworks = (connectedWifi != null ? 1 : 0) |
+ + (visibleWifis != null ? visibleWifis.size() : 0) + (connectedCell != null ? 1 : 0) |
+ + (visibleCells != null ? visibleCells.size() : 0); |
+ if (numVisibleNetworks == 0) { |
+ // No data to encode. |
+ return null; |
+ } |
+ |
+ int i = 0; |
+ PartnerLocationDescriptor.VisibleNetwork[] protoNetworks = |
+ new PartnerLocationDescriptor.VisibleNetwork[numVisibleNetworks]; |
+ if (connectedWifi != null) { |
+ protoNetworks[i++] = connectedWifi.toProto(true); |
+ } |
+ if (visibleWifis != null) { |
+ for (VisibleWifi visibleWifi : visibleWifis) { |
+ protoNetworks[i++] = visibleWifi.toProto(false); |
+ } |
+ } |
+ if (connectedCell != null) { |
+ protoNetworks[i++] = connectedCell.toProto(true); |
+ } |
+ if (visibleCells != null) { |
+ for (VisibleCell visibleCell : visibleCells) { |
+ protoNetworks[i++] = visibleCell.toProto(false); |
+ } |
+ } |
+ |
+ PartnerLocationDescriptor.LocationDescriptor locationDescriptor = |
+ new PartnerLocationDescriptor.LocationDescriptor(); |
+ locationDescriptor.role = PartnerLocationDescriptor.CURRENT_LOCATION; |
+ locationDescriptor.producer = PartnerLocationDescriptor.DEVICE_LOCATION; |
+ locationDescriptor.visibleNetwork = protoNetworks; |
+ |
+ return encodeLocationDescriptor(locationDescriptor); |
+ } |
+ |
+ @Nullable |
+ @VisibleForTesting |
+ static VisibleNetworks trimVisibleNetworks(@Nullable VisibleNetworks visibleNetworks) { |
+ if (visibleNetworks == null || visibleNetworks.isEmpty()) { |
+ return null; |
+ } |
+ // Trim visible networks to only include a limited number of visible not-conntected networks |
+ // based on flag. |
+ VisibleCell connectedCell = visibleNetworks.connectedCell(); |
+ VisibleWifi connectedWifi = visibleNetworks.connectedWifi(); |
+ Set<VisibleCell> visibleCells = visibleNetworks.allVisibleCells(); |
+ Set<VisibleWifi> visibleWifis = visibleNetworks.allVisibleWifis(); |
+ VisibleCell extraVisibleCell = null; |
+ VisibleWifi extraVisibleWifi = null; |
+ if (shouldExcludeVisibleWifi(connectedWifi)) { |
+ // Trim the connected wifi. |
+ connectedWifi = null; |
+ } |
+ // Select the extra visible cell. |
+ if (visibleCells != null) { |
+ for (VisibleCell candidateCell : visibleCells) { |
+ if (ApiCompatibilityUtils.objectEquals(connectedCell, candidateCell)) { |
+ // Do not include this candidate cell, since its already the connected one. |
+ continue; |
+ } |
+ // Add it and since we only want one, stop iterating over other cells. |
+ extraVisibleCell = candidateCell; |
+ break; |
+ } |
+ } |
+ // Select the extra visible wifi. |
+ if (visibleWifis != null) { |
+ for (VisibleWifi candidateWifi : visibleWifis) { |
+ if (shouldExcludeVisibleWifi(candidateWifi)) { |
+ // Do not include this candidate wifi. |
+ continue; |
+ } |
+ if (ApiCompatibilityUtils.objectEquals(connectedWifi, candidateWifi)) { |
+ // Replace the connected, since the candidate will have level. This is because |
+ // the android APIs exposing connected WIFI do not expose level, while the ones |
+ // exposing visible wifis expose level. |
+ connectedWifi = candidateWifi; |
+ // Do not include this candidate wifi, since its already the connected one. |
+ continue; |
+ } |
+ // Keep the one with stronger level (since it's negative, this is the smaller value) |
+ if (extraVisibleWifi == null || extraVisibleWifi.level() > candidateWifi.level()) { |
+ extraVisibleWifi = candidateWifi; |
+ } |
+ } |
+ } |
+ |
+ if (connectedCell == null && connectedWifi == null && extraVisibleCell == null |
+ && extraVisibleWifi == null) { |
+ return null; |
+ } |
+ |
+ return VisibleNetworks.create(connectedWifi, connectedCell, |
+ extraVisibleWifi != null ? CollectionUtil.newHashSet(extraVisibleWifi) : null, |
+ extraVisibleCell != null ? CollectionUtil.newHashSet(extraVisibleCell) : null); |
+ } |
+ |
+ /** |
+ * Returns whether the provided {@link VisibleWifi} should be excluded. This can happen if the |
+ * network is opted out (ssid contains "_nomap" or "_optout"). |
+ */ |
+ private static boolean shouldExcludeVisibleWifi(@Nullable VisibleWifi visibleWifi) { |
+ if (visibleWifi == null || visibleWifi.bssid() == null) { |
+ return true; |
+ } |
+ String ssid = visibleWifi.ssid(); |
+ if (ssid == null) { |
+ // No ssid, so the networks is not opted out and should not be excluded. |
+ return false; |
+ } |
+ // Optimization to avoid costly toLowerCase() in most cases. |
+ if (ssid.indexOf('_') < 0) { |
+ // No "_nomap" or "_optout". |
+ return false; |
+ } |
+ String ssidLowerCase = ssid.toLowerCase(Locale.ENGLISH); |
+ return ssidLowerCase.contains(SSID_NOMAP) || ssidLowerCase.contains(SSID_OPTOUT); |
+ } |
} |