chromium/services/device/geolocation/android/java/src/org/chromium/device/geolocation/LocationProviderAndroid.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 android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.ThreadUtils;

import java.util.List;

/**
 * This is a LocationProvider using Android APIs [1]. It is a separate class for clarity
 * so that it can manage all processing completely on the UI thread. The container class
 * ensures that the start/stop calls into this class are done on the UI thread.
 *
 * [1] https://developer.android.com/reference/android/location/package-summary.html
 */
public class LocationProviderAndroid implements LocationListener, LocationProvider {
    private static final String TAG = "LocationProvider";

    private LocationManager mLocationManager;
    private boolean mIsRunning;

    LocationProviderAndroid() {}

    @Override
    public void start(boolean enableHighAccuracy) {
        ThreadUtils.assertOnUiThread();
        unregisterFromLocationUpdates();
        registerForLocationUpdates(enableHighAccuracy);
    }

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

    @Override
    public boolean isRunning() {
        ThreadUtils.assertOnUiThread();
        return mIsRunning;
    }

    @Override
    public void onLocationChanged(Location location) {
        // Callbacks from the system location service are queued to this thread, so it's
        // possible that we receive callbacks after unregistering. At this point, the
        // native object will no longer exist.
        if (mIsRunning) {
            LocationProviderAdapter.onNewLocationAvailable(location);
        }
    }

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {}

    @Override
    public void onProviderEnabled(String provider) {}

    @Override
    public void onProviderDisabled(String provider) {}

    public void setLocationManagerForTesting(LocationManager manager) {
        var oldValue = mLocationManager;
        mLocationManager = manager;
        ResettersForTesting.register(() -> mLocationManager = oldValue);
    }

    private void createLocationManagerIfNeeded() {
        if (mLocationManager != null) return;
        mLocationManager =
                (LocationManager)
                        ContextUtils.getApplicationContext()
                                .getSystemService(Context.LOCATION_SERVICE);
        if (mLocationManager == null) {
            Log.e(TAG, "Could not get location manager.");
        }
    }

    /** Registers this object with the location service. */
    private void registerForLocationUpdates(boolean enableHighAccuracy) {
        createLocationManagerIfNeeded();
        if (usePassiveOneShotLocation()) return;

        assert !mIsRunning;
        mIsRunning = true;

        // We're running on the main thread. The C++ side is responsible to
        // bounce notifications to the Geolocation thread as they arrive in the mainLooper.
        try {
            Criteria criteria = new Criteria();
            Context context = ContextUtils.getApplicationContext();
            if (enableHighAccuracy
                    && context.checkCallingOrSelfPermission(
                                    Manifest.permission.ACCESS_FINE_LOCATION)
                            == PackageManager.PERMISSION_GRANTED) {
                criteria.setAccuracy(Criteria.ACCURACY_FINE);
            }
            mLocationManager.requestLocationUpdates(
                    0, 0, criteria, this, ThreadUtils.getUiThreadLooper());
        } catch (SecurityException e) {
            Log.e(
                    TAG,
                    "Caught security exception while registering for location updates "
                            + "from the system. The application does not have sufficient "
                            + "geolocation permissions.");
            unregisterFromLocationUpdates();
            // Propagate an error to JavaScript, this can happen in case of WebView
            // when the embedding app does not have sufficient permissions.
            LocationProviderAdapter.newErrorAvailable(
                    "application does not have sufficient geolocation permissions.");
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "Caught IllegalArgumentException registering for location updates.");
            unregisterFromLocationUpdates();
            assert false;
        }
    }

    /** Unregisters this object from the location service. */
    private void unregisterFromLocationUpdates() {
        if (!mIsRunning) return;
        mIsRunning = false;
        mLocationManager.removeUpdates(this);
    }

    private boolean usePassiveOneShotLocation() {
        if (!isOnlyPassiveLocationProviderEnabled()) {
            return false;
        }

        // Do not request a location update if the only available location provider is
        // the passive one. Make use of the last known location and call
        // onNewLocationAvailable directly.
        final Location location =
                mLocationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER);
        if (location != null) {
            ThreadUtils.assertOnUiThread();
            LocationProviderAdapter.onNewLocationAvailable(location);
        }
        return true;
    }

    /*
     * Checks if the passive location provider is the only provider available
     * in the system.
     */
    private boolean isOnlyPassiveLocationProviderEnabled() {
        final List<String> providers = mLocationManager.getProviders(true);
        return providers != null
                && providers.size() == 1
                && providers.get(0).equals(LocationManager.PASSIVE_PROVIDER);
    }
}