// 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.ContextUtils;
import org.chromium.base.annotations.CalledByNative;
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;
    }

    @CalledByNative
    public static boolean hasGeolocationPermission() {
        Context context = ContextUtils.getApplicationContext();
        return hasGeolocationPermission(context);
    }

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