chromium/services/device/geolocation/android/java/src/org/chromium/device/geolocation/LocationProviderGmsCore.java

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.device.geolocation;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;

import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;

import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.gms.ChromiumPlayServicesAvailability;

/**
 * This is a LocationProvider using Google Play Services.
 *
 * https://developers.google.com/android/reference/com/google/android/gms/location/package-summary
 */
public class LocationProviderGmsCore implements LocationProvider {
    private static final String TAG = "LocationProvider";

    // Values for the LocationRequest's setInterval for normal and high accuracy, respectively.
    private static final long UPDATE_INTERVAL_MS = 1000;
    private static final long UPDATE_INTERVAL_FAST_MS = 500;

    private final Context mContext;
    private final FusedLocationProviderClient mClient;

    private LocationCallback mLocationCallback;

    public static boolean isGooglePlayServicesAvailable(Context context) {
        return ChromiumPlayServicesAvailability.isGooglePlayServicesAvailable(context);
    }

    LocationProviderGmsCore(Context context) {
        Log.i(TAG, "Google Play Services");
        mContext = context;
        mClient = LocationServices.getFusedLocationProviderClient(context);
        assert mClient != null;
    }

    LocationProviderGmsCore(Context context, FusedLocationProviderClient client) {
        mContext = context;
        mClient = client;
    }

    // LocationProvider implementation
    @Override
    public void start(boolean enableHighAccuracy) {
        ThreadUtils.assertOnUiThread();

        LocationRequest locationRequest = LocationRequest.create();
        if (mContext.checkCallingOrSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
            // Workaround for a bug in Google Play Services where, if an app only has
            // ACCESS_COARSE_LOCATION, trying to request PRIORITY_HIGH_ACCURACY will throw a
            // SecurityException even on Android S. See: b/184924939.
            enableHighAccuracy = false;
        }

        if (enableHighAccuracy) {
            // With enableHighAccuracy, request a faster update interval and configure the provider
            // for high accuracy mode.
            locationRequest
                    .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
                    .setInterval(UPDATE_INTERVAL_FAST_MS);
        } else {
            // Use balanced mode by default. In this mode, the API will prefer the network provider
            // but may use sensor data (for instance, GPS) if high accuracy is requested by another
            // app.
            locationRequest
                    .setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY)
                    .setInterval(UPDATE_INTERVAL_MS);
        }

        if (mLocationCallback != null) {
            mClient.removeLocationUpdates(mLocationCallback);
        }

        mLocationCallback =
                new LocationCallback() {
                    @Override
                    public void onLocationResult(LocationResult locationResult) {
                        if (locationResult == null) {
                            return;
                        }
                        LocationProviderAdapter.onNewLocationAvailable(
                                locationResult.getLastLocation());
                    }
                };

        try {
            // Request updates on UI Thread replicating LocationProviderAndroid's behaviour.
            mClient.requestLocationUpdates(
                            locationRequest, mLocationCallback, ThreadUtils.getUiThreadLooper())
                    .addOnFailureListener(
                            (e) -> {
                                Log.e(TAG, "mClient.requestLocationUpdates() " + e);
                                LocationProviderAdapter.newErrorAvailable(
                                        "Failed to request location updates: " + e.toString());
                            });
        } catch (IllegalStateException e) {
            // IllegalStateException is thrown "If this method is executed in a thread that has not
            // called Looper.prepare()".
            Log.e(TAG, "mClient.requestLocationUpdates() " + e);
            LocationProviderAdapter.newErrorAvailable(
                    "Failed to request location updates: " + e.toString());
            assert false;
        } catch (SecurityException e) {
            // SecurityException is thrown when the app is missing location permissions. See
            // crbug.com/731271.
            Log.e(TAG, "mClient.requestLocationUpdates() missing permissions " + e);
            LocationProviderAdapter.newErrorAvailable(
                    "Failed to request location updates due to permissions: " + e.toString());
        }
    }

    @Override
    public void stop() {
        ThreadUtils.assertOnUiThread();

        mClient.removeLocationUpdates(mLocationCallback);
        mLocationCallback = null;
    }

    @Override
    public boolean isRunning() {
        assert ThreadUtils.runningOnUiThread();
        return mLocationCallback != null;
    }
}