| 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);
|
| + }
|
| }
|
|
|