// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.omnibox.geo; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.location.Location; import android.net.Uri; import android.os.Build; import android.os.Process; import android.util.Base64; import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.metrics.RecordHistogram; import org.chromium.chrome.browser.preferences.website.ContentSetting; import org.chromium.chrome.browser.preferences.website.GeolocationInfo; import org.chromium.chrome.browser.util.UrlUtilities; import java.util.Locale; /** * Provides methods for building the X-Geo HTTP header, which provides device location to a server * when making an HTTP request. * * X-Geo header spec: https://goto.google.com/xgeospec. */ public class GeolocationHeader { // Values for the histogram Geolocation.HeaderSentOrNot. Values 1, 5, 6, and 7 are defined in // histograms.xml and should not be used in other ways. public static final int UMA_LOCATION_DISABLED_FOR_GOOGLE_DOMAIN = 0; public static final int UMA_LOCATION_NOT_AVAILABLE = 2; public static final int UMA_LOCATION_STALE = 3; public static final int UMA_HEADER_SENT = 4; public static final int UMA_LOCATION_DISABLED_FOR_CHROME_APP = 5; public static final int UMA_MAX = 8; /** The maximum age in milliseconds of a location that we'll send in an X-Geo header. */ private static final int MAX_LOCATION_AGE = 24 * 60 * 60 * 1000; // 24 hours /** The maximum age in milliseconds of a location before we'll request a refresh. */ private static final int REFRESH_LOCATION_AGE = 5 * 60 * 1000; // 5 minutes private static final String HTTPS_SCHEME = "https"; /** * 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). * * @param context The Context used to get the device location. */ public static void primeLocationForGeoHeader(Context context) { if (!hasGeolocationPermission(context)) return; GeolocationTracker.refreshLastKnownLocation(context, REFRESH_LOCATION_AGE); } /** * Returns whether the X-Geo header is allowed to be sent for the current URL. * * @param context The Context used to get the device location. * @param url The URL of the request with which this header will be sent. * @param isIncognito Whether the request will happen in an incognito tab. */ public static boolean isGeoHeaderEnabledForUrl(Context context, String url, boolean isIncognito) { return isGeoHeaderEnabledForUrl(context, url, isIncognito, false); } private static boolean isGeoHeaderEnabledForUrl(Context context, String url, boolean isIncognito, boolean recordUma) { // Only send X-Geo in normal mode. if (isIncognito) return false; // Only send X-Geo header to Google domains. if (!UrlUtilities.nativeIsGoogleSearchUrl(url)) return false; Uri uri = Uri.parse(url); if (!HTTPS_SCHEME.equals(uri.getScheme())) return false; if (!hasGeolocationPermission(context)) { if (recordUma) recordHistogram(UMA_LOCATION_DISABLED_FOR_CHROME_APP); return false; } // Only send X-Geo header if the user hasn't disabled geolocation for url. if (isLocationDisabledForUrl(uri, isIncognito)) { if (recordUma) recordHistogram(UMA_LOCATION_DISABLED_FOR_GOOGLE_DOMAIN); return false; } return true; } /** * Returns an X-Geo HTTP header string if: * 1. The current mode is not incognito. * 2. The url is a google search URL (e.g. www.google.co.uk/search?q=cars), and * 3. The user has not disabled sharing location with this url, and * 4. There is a valid and recent location available. * * Returns null otherwise. * * @param context The Context used to get the device location. * @param url The URL of the request with which this header will be sent. * @param isIncognito Whether the request will happen in an incognito tab. * @return The X-Geo header string or null. */ public static String getGeoHeader(Context context, String url, boolean isIncognito) { if (!isGeoHeaderEnabledForUrl(context, url, isIncognito, true)) { return null; } // Only send X-Geo header if there's a fresh location available. Location location = GeolocationTracker.getLastKnownLocation(context); if (location == null) { recordHistogram(UMA_LOCATION_NOT_AVAILABLE); return null; } if (GeolocationTracker.getLocationAge(location) > MAX_LOCATION_AGE) { recordHistogram(UMA_LOCATION_STALE); return null; } recordHistogram(UMA_HEADER_SENT); // Timestamp in microseconds since the UNIX epoch. long timestamp = location.getTime() * 1000; // Latitude times 1e7. int latitude = (int) (location.getLatitude() * 10000000); // Longitude times 1e7. int longitude = (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, latitude, longitude, radius); String locationBase64 = new String(Base64.encode(locationAscii.getBytes(), Base64.NO_WRAP)); return "X-Geo: a " + locationBase64; } static boolean hasGeolocationPermission(Context context) { int pid = Process.myPid(); int uid = Process.myUid(); if (ApiCompatibilityUtils.checkPermission( context, Manifest.permission.ACCESS_COARSE_LOCATION, pid, uid) != PackageManager.PERMISSION_GRANTED) { return false; } // Work around a bug in OnePlus2 devices running Lollipop, where the NETWORK_PROVIDER // incorrectly requires FINE_LOCATION permission (it should only require COARSE_LOCATION // permission). http://crbug.com/580733 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M && ApiCompatibilityUtils.checkPermission( context, Manifest.permission.ACCESS_FINE_LOCATION, pid, uid) != PackageManager.PERMISSION_GRANTED) { return false; } return true; } /** * Returns true if the user has disabled sharing their location with url (e.g. via the * geolocation infobar). If the user has not chosen a preference for url and url uses the https * scheme, this considers the user's preference for url with the http scheme instead. */ static boolean isLocationDisabledForUrl(Uri uri, boolean isIncognito) { GeolocationInfo locationSettings = new GeolocationInfo(uri.toString(), null, isIncognito); ContentSetting locationPermission = locationSettings.getContentSetting(); // If no preference has been chosen and the scheme is https, fall back to the preference for // this same host over http with no explicit port number. if (locationPermission == null || locationPermission == ContentSetting.ASK) { String scheme = uri.getScheme(); if (scheme != null && scheme.toLowerCase(Locale.US).equals("https") && uri.getAuthority() != null && uri.getUserInfo() == null) { String urlWithHttp = "http://" + uri.getHost(); locationSettings = new GeolocationInfo(urlWithHttp, null, isIncognito); locationPermission = locationSettings.getContentSetting(); } } return locationPermission == ContentSetting.BLOCK; } /** Records a data point for the Geolocation.HeaderSentOrNot histogram. */ private static void recordHistogram(int result) { RecordHistogram.recordEnumeratedHistogram("Geolocation.HeaderSentOrNot", result, UMA_MAX); } }