public final class

LocationManagerCompat

extends java.lang.Object

 java.lang.Object

↳androidx.core.location.LocationManagerCompat

Gradle dependencies

compile group: 'androidx.core', name: 'core', version: '1.9.0-alpha04'

  • groupId: androidx.core
  • artifactId: core
  • version: 1.9.0-alpha04

Artifact androidx.core:core:1.9.0-alpha04 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.core:core com.android.support:support-compat

Overview

Helper for accessing features in .

Summary

Methods
public static voidgetCurrentLocation(LocationManager locationManager, java.lang.String provider, CancellationSignal cancellationSignal, java.util.concurrent.Executor executor, Consumer<Location> consumer)

Asynchronously returns a single current location fix from the given provider.

public static java.lang.StringgetGnssHardwareModelName(LocationManager locationManager)

Returns the model name (including vendor and hardware/software version) of the GNSS hardware driver, or null if this information is not available.

public static intgetGnssYearOfHardware(LocationManager locationManager)

Returns the model year of the GNSS hardware and software build, or 0 if the model year is before 2016.

public static booleanhasProvider(LocationManager locationManager, java.lang.String provider)

Returns true if the given location provider exists on this device, irrespective of whether it is currently enabled or not.

public static booleanisLocationEnabled(LocationManager locationManager)

Returns the current enabled/disabled state of location.

public static booleanregisterGnssStatusCallback(LocationManager locationManager, java.util.concurrent.Executor executor, GnssStatusCompat.Callback callback)

Registers a platform agnostic GnssStatusCompat.Callback.

public static booleanregisterGnssStatusCallback(LocationManager locationManager, GnssStatusCompat.Callback callback, Handler handler)

Registers a platform agnostic GnssStatusCompat.Callback.

public static voidremoveUpdates(LocationManager locationManager, LocationListenerCompat listener)

Removes all location updates for the specified .

public static voidrequestLocationUpdates(LocationManager locationManager, java.lang.String provider, LocationRequestCompat locationRequest, java.util.concurrent.Executor executor, LocationListenerCompat listener)

Register for location updates from the specified provider, using a LocationRequestCompat, and a callback on the specified java.util.concurrent.Executor.

public static voidrequestLocationUpdates(LocationManager locationManager, java.lang.String provider, LocationRequestCompat locationRequest, LocationListenerCompat listener, Looper looper)

Register for location updates from the specified provider, using a LocationRequestCompat, and a callback on the specified .

public static voidunregisterGnssStatusCallback(LocationManager locationManager, GnssStatusCompat.Callback callback)

Unregisters a platform agnostic GnssStatusCompat.Callback.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Methods

public static boolean isLocationEnabled(LocationManager locationManager)

Returns the current enabled/disabled state of location.

NOTE: Calling this method on API levels prior to 20 may require the or permission if run on non-standard Android devices. The vast majority of devices should not require either permission to be present for this method.

Returns:

true if location is enabled or false if location is disabled

public static boolean hasProvider(LocationManager locationManager, java.lang.String provider)

Returns true if the given location provider exists on this device, irrespective of whether it is currently enabled or not. If called on Android Q and below for the , this method may return incorrect results if the client does not hold at least the permission.

public static void getCurrentLocation(LocationManager locationManager, java.lang.String provider, CancellationSignal cancellationSignal, java.util.concurrent.Executor executor, Consumer<Location> consumer)

Asynchronously returns a single current location fix from the given provider. This may activate sensors in order to compute a new location. The given callback will be invoked once and only once, either with a valid location or with a null location if the provider was unable to generate a valid location.

A client may supply an optional CancellationSignal. If this is used to cancel the operation, no callback should be expected after the cancellation.

This method may return locations from the very recent past (on the order of several seconds), but will never return older locations (for example, several minutes old or older). Clients may rely upon the guarantee that if this method returns a location, it will represent the best estimation of the location of the device in the present moment.

Clients calling this method from the background may notice that the method fails to determine a valid location fix more often than while in the foreground. Background applications may be throttled in their location accesses to some degree.

public static void requestLocationUpdates(LocationManager locationManager, java.lang.String provider, LocationRequestCompat locationRequest, java.util.concurrent.Executor executor, LocationListenerCompat listener)

Register for location updates from the specified provider, using a LocationRequestCompat, and a callback on the specified java.util.concurrent.Executor.

See for more information.

public static void requestLocationUpdates(LocationManager locationManager, java.lang.String provider, LocationRequestCompat locationRequest, LocationListenerCompat listener, Looper looper)

Register for location updates from the specified provider, using a LocationRequestCompat, and a callback on the specified .

See for more information.

public static void removeUpdates(LocationManager locationManager, LocationListenerCompat listener)

Removes all location updates for the specified .

See for more information.

public static java.lang.String getGnssHardwareModelName(LocationManager locationManager)

Returns the model name (including vendor and hardware/software version) of the GNSS hardware driver, or null if this information is not available. No device-specific serial number or ID is returned from this API.

public static int getGnssYearOfHardware(LocationManager locationManager)

Returns the model year of the GNSS hardware and software build, or 0 if the model year is before 2016.

public static boolean registerGnssStatusCallback(LocationManager locationManager, GnssStatusCompat.Callback callback, Handler handler)

Registers a platform agnostic GnssStatusCompat.Callback. See and .

See also: LocationManagerCompat.registerGnssStatusCallback(LocationManager, Executor, GnssStatusCompat.Callback)

public static boolean registerGnssStatusCallback(LocationManager locationManager, java.util.concurrent.Executor executor, GnssStatusCompat.Callback callback)

Registers a platform agnostic GnssStatusCompat.Callback. See and .

Internally, this API will always utilize GnssStatus APIs and instances on Android N and above, and will always utilize GpsStatus APIs and instances below Android N. Callbacks will always occur on the given executor.

If invoked on Android M or below, this will result in GpsStatus registration being run on either the current Looper or main Looper. If the thread this function is invoked on is different from that Looper, the caller must ensure that the Looper thread cannot be blocked by the thread this function is invoked on. The easiest way to avoid this is to ensure this function is invoked on a Looper thread.

public static void unregisterGnssStatusCallback(LocationManager locationManager, GnssStatusCompat.Callback callback)

Unregisters a platform agnostic GnssStatusCompat.Callback. See and .

Source

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.core.location;

import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.provider.Settings.Secure.LOCATION_MODE;
import static android.provider.Settings.Secure.LOCATION_MODE_OFF;

import static androidx.core.location.LocationCompat.getElapsedRealtimeMillis;

import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

import android.annotation.SuppressLint;
import android.content.Context;
import android.location.GnssStatus;
import android.location.GpsStatus;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationRequest;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.text.TextUtils;

import androidx.annotation.DoNotInline;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
import androidx.collection.SimpleArrayMap;
import androidx.core.os.CancellationSignal;
import androidx.core.os.ExecutorCompat;
import androidx.core.util.Consumer;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;

import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeoutException;

/**
 * Helper for accessing features in {@link LocationManager}.
 */
@SuppressWarnings({"deprecation", "unused"})
public final class LocationManagerCompat {

    private static final long GET_CURRENT_LOCATION_TIMEOUT_MS = 30 * 1000;
    private static final long MAX_CURRENT_LOCATION_AGE_MS = 10 * 1000;
    private static final long PRE_N_LOOPER_TIMEOUT_S = 5;

    private static Field sContextField;

    /**
     * Returns the current enabled/disabled state of location.
     *
     * <p>NOTE: Calling this method on API levels prior to 20 <i>may</i> require the
     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} or
     * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION}
     * permission if run on non-standard Android devices. The vast majority of devices should not
     * require either permission to be present for this method.
     *
     * @return {@code true} if location is enabled or {@code false} if location is disabled
     */
    @SuppressWarnings("JavaReflectionMemberAccess")
    public static boolean isLocationEnabled(@NonNull LocationManager locationManager) {
        if (VERSION.SDK_INT >= 28) {
            return Api28Impl.isLocationEnabled(locationManager);
        }

        if (VERSION.SDK_INT <= 19) {
            // KitKat and below have pointless location permission requirements when using
            // isProviderEnabled(). Instead, we attempt to reflect a context so that we can query
            // the underlying setting. If this fails, we fallback to isProviderEnabled() which may
            // require the caller to hold location permissions.
            try {
                if (sContextField == null) {
                    sContextField = LocationManager.class.getDeclaredField("mContext");
                    sContextField.setAccessible(true);
                }
                Context context = (Context) sContextField.get(locationManager);

                if (context != null) {
                    if (VERSION.SDK_INT == 19) {
                        return Secure.getInt(context.getContentResolver(), LOCATION_MODE,
                                LOCATION_MODE_OFF) != LOCATION_MODE_OFF;
                    } else {
                        return !TextUtils.isEmpty(
                                Settings.Secure.getString(context.getContentResolver(),
                                        Settings.Secure.LOCATION_PROVIDERS_ALLOWED));
                    }
                }
            } catch (ClassCastException | SecurityException | NoSuchFieldException
                    | IllegalAccessException e) {
                // oh well, fallback to isProviderEnabled()
            }
        }

        return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
            || locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
    }

    /**
     * Returns true if the given location provider exists on this device, irrespective of whether
     * it is currently enabled or not. If called on Android Q and below for the
     * {@link LocationManager#FUSED_PROVIDER}, this method may return incorrect results if the
     * client does not hold at least the {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}
     * permission.
     */
    public static boolean hasProvider(@NonNull LocationManager locationManager,
            @NonNull String provider) {
        if (VERSION.SDK_INT >= 31) {
            return Api31Impl.hasProvider(locationManager, provider);
        }

        // will not work for the FUSED provider by default
        if (locationManager.getAllProviders().contains(provider)) {
            return true;
        }

        try {
            // Q and below have pointless location permission requirements when using getProvider()
            return locationManager.getProvider(provider) != null;
        } catch (SecurityException ignored) {
        }

        return false;
    }

    /**
     * Asynchronously returns a single current location fix from the given provider. This may
     * activate sensors in order to compute a new location. The given callback will be invoked once
     * and only once, either with a valid location or with a null location if the provider was
     * unable to generate a valid location.
     *
     * <p>A client may supply an optional {@link CancellationSignal}. If this is used to cancel the
     * operation, no callback should be expected after the cancellation.
     *
     * <p>This method may return locations from the very recent past (on the order of several
     * seconds), but will never return older locations (for example, several minutes old or older).
     * Clients may rely upon the guarantee that if this method returns a location, it will represent
     * the best estimation of the location of the device in the present moment.
     *
     * <p>Clients calling this method from the background may notice that the method fails to
     * determine a valid location fix more often than while in the foreground. Background
     * applications may be throttled in their location accesses to some degree.
     */
    @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
    public static void getCurrentLocation(@NonNull LocationManager locationManager,
            @NonNull String provider, @Nullable CancellationSignal cancellationSignal,
            @NonNull Executor executor, @NonNull final Consumer<Location> consumer) {
        if (VERSION.SDK_INT >= 30) {
            Api30Impl.getCurrentLocation(locationManager, provider, cancellationSignal, executor,
                    consumer);
            return;
        }

        if (cancellationSignal != null) {
            cancellationSignal.throwIfCanceled();
        }

        final Location location = locationManager.getLastKnownLocation(provider);
        if (location != null) {
            long locationAgeMs =
                    SystemClock.elapsedRealtime() - getElapsedRealtimeMillis(location);
            if (locationAgeMs < MAX_CURRENT_LOCATION_AGE_MS) {
                executor.execute(() -> consumer.accept(location));
                return;
            }
        }

        final CancellableLocationListener listener =
                new CancellableLocationListener(locationManager, executor, consumer);
        locationManager.requestLocationUpdates(provider, 0, 0, listener,
                Looper.getMainLooper());

        if (cancellationSignal != null) {
            cancellationSignal.setOnCancelListener(listener::cancel);
        }

        listener.startTimeout(GET_CURRENT_LOCATION_TIMEOUT_MS);
    }

    @GuardedBy("sLocationListeners")
    static final WeakHashMap<LocationListenerKey, WeakReference<LocationListenerTransport>>
            sLocationListeners = new WeakHashMap<>();

    /**
     * Register for location updates from the specified provider, using a
     * {@link LocationRequestCompat}, and a callback on the specified {@link Executor}.
     *
     * <p>See
     * {@link LocationManager#requestLocationUpdates(String, LocationRequest, Executor,
     * LocationListener)} for more information.
     */
    @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
    public static void requestLocationUpdates(@NonNull LocationManager locationManager,
            @NonNull String provider,
            @NonNull LocationRequestCompat locationRequest,
            @NonNull Executor executor,
            @NonNull LocationListenerCompat listener) {
        if (VERSION.SDK_INT >= 31) {
            Api31Impl.requestLocationUpdates(locationManager, provider,
                    locationRequest.toLocationRequest(), executor, listener);
            return;
        }

        if (VERSION.SDK_INT >= 30 && Api30Impl.tryRequestLocationUpdates(
                locationManager, provider, locationRequest, executor, listener)) {
            return;
        }

        LocationListenerTransport transport = new LocationListenerTransport(
                new LocationListenerKey(provider, listener), executor);

        if (VERSION.SDK_INT >= 19 && Api19Impl.tryRequestLocationUpdates(
                locationManager, provider, locationRequest, transport)) {
            return;
        }

        synchronized (sLocationListeners) {
            locationManager.requestLocationUpdates(provider, locationRequest.getIntervalMillis(),
                    locationRequest.getMinUpdateDistanceMeters(), transport,
                    Looper.getMainLooper());
            registerLocationListenerTransport(locationManager, transport);
        }
    }

    @GuardedBy("sLocationListeners")
    @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
    static void registerLocationListenerTransport(LocationManager locationManager,
            LocationListenerTransport transport) {
        WeakReference<LocationListenerTransport> oldRef =
                sLocationListeners.put(transport.getKey(), new WeakReference<>(transport));
        LocationListenerTransport oldTransport = oldRef != null ? oldRef.get() : null;
        if (oldTransport != null) {
            oldTransport.unregister();
            locationManager.removeUpdates(oldTransport);
        }
    }

    /**
     * Register for location updates from the specified provider, using a
     * {@link LocationRequestCompat}, and a callback on the specified {@link Looper}.
     *
     * <p>See
     * {@link LocationManager#requestLocationUpdates(String, LocationRequest, Executor,
     * LocationListener)} for more information.
     */
    @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
    public static void requestLocationUpdates(@NonNull LocationManager locationManager,
            @NonNull String provider,
            @NonNull LocationRequestCompat locationRequest,
            @NonNull LocationListenerCompat listener,
            @NonNull Looper looper) {
        if (VERSION.SDK_INT >= 31) {
            Api31Impl.requestLocationUpdates(locationManager, provider,
                    locationRequest.toLocationRequest(),
                    ExecutorCompat.create(new Handler(looper)), listener);
            return;
        }

        if (VERSION.SDK_INT >= 19 && Api19Impl.tryRequestLocationUpdates(
                    locationManager, provider, locationRequest, listener, looper)) {
            return;
        }

        locationManager.requestLocationUpdates(provider, locationRequest.getIntervalMillis(),
                locationRequest.getMinUpdateDistanceMeters(), listener, looper);
    }

    /**
     * Removes all location updates for the specified {@link LocationListener}.
     *
     * <p>See {@link LocationManager#removeUpdates(LocationListener)} for more information.
     */
    @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
    public static void removeUpdates(@NonNull LocationManager locationManager,
            @NonNull LocationListenerCompat listener) {
        synchronized (sLocationListeners) {
            ArrayList<LocationListenerKey> cleanup = null;
            for (WeakReference<LocationListenerTransport> transportRef :
                    sLocationListeners.values()) {
                LocationListenerTransport transport = transportRef.get();
                if (transport == null) {
                    continue;
                }
                LocationListenerKey key = transport.getKey();
                if (key.mListener == listener) {
                    if (cleanup == null) {
                        cleanup = new ArrayList<>();
                    }
                    cleanup.add(key);
                    transport.unregister();
                    locationManager.removeUpdates(transport);
                }
            }
            if (cleanup != null) {
                for (LocationListenerKey key : cleanup) {
                    sLocationListeners.remove(key);
                }
            }
        }

        // a given listener could have been registered both with an executor and a looper, so we
        // need to remove all possible cases
        locationManager.removeUpdates(listener);
    }

    /**
     * Returns the model name (including vendor and hardware/software version) of the GNSS
     * hardware driver, or null if this information is not available.
     *
     * No device-specific serial number or ID is returned from this API.
     */
    @Nullable
    public static String getGnssHardwareModelName(@NonNull LocationManager locationManager) {
        if (VERSION.SDK_INT >= 28) {
            return Api28Impl.getGnssHardwareModelName(locationManager);
        } else {
            return null;
        }
    }

    /**
     * Returns the model year of the GNSS hardware and software build, or 0 if the model year is
     * before 2016.
     */
    public static int getGnssYearOfHardware(@NonNull LocationManager locationManager) {
        if (VERSION.SDK_INT >= 28) {
            return Api28Impl.getGnssYearOfHardware(locationManager);
        } else {
            return 0;
        }
    }

    // allows lazy instantiation since most processes do not use GNSS APIs
    private static class GnssLazyLoader {
        @GuardedBy("sGnssStatusListeners")
        static final SimpleArrayMap<Object, Object> sGnssStatusListeners =
                new SimpleArrayMap<>();
    }

    /**
     * Registers a platform agnostic {@link GnssStatusCompat.Callback}. See
     * {@link LocationManager#addGpsStatusListener(GpsStatus.Listener)} and
     * {@link LocationManager#registerGnssStatusCallback(GnssStatus.Callback, Handler)}.
     *
     * @see #registerGnssStatusCallback(LocationManager, Executor, GnssStatusCompat.Callback)
     */
    @RequiresPermission(ACCESS_FINE_LOCATION)
    public static boolean registerGnssStatusCallback(@NonNull LocationManager locationManager,
            @NonNull GnssStatusCompat.Callback callback, @NonNull Handler handler) {
        if (VERSION.SDK_INT >= VERSION_CODES.R) {
            return registerGnssStatusCallback(locationManager, ExecutorCompat.create(handler),
                callback);
        } else {
            return registerGnssStatusCallback(locationManager, new InlineHandlerExecutor(handler),
                    callback);
        }
    }

    /**
     * Registers a platform agnostic {@link GnssStatusCompat.Callback}. See
     * {@link LocationManager#addGpsStatusListener(GpsStatus.Listener)} and
     * {@link LocationManager#registerGnssStatusCallback(Executor, GnssStatus.Callback)}.
     *
     * <p>Internally, this API will always utilize GnssStatus APIs and instances on Android N and
     * above, and will always utilize GpsStatus APIs and instances below Android N. Callbacks will
     * always occur on the given executor.
     *
     * <p>If invoked on Android M or below, this will result in GpsStatus registration being run on
     * either the current Looper or main Looper. If the thread this function is invoked on is
     * different from that Looper, the caller must ensure that the Looper thread cannot be blocked
     * by the thread this function is invoked on. The easiest way to avoid this is to ensure this
     * function is invoked on a Looper thread.
     *
     * @throws IllegalStateException on Android M or below, if the current Looper or main Looper
     *                               is blocked by the thread this function is invoked on
     */
    @RequiresPermission(ACCESS_FINE_LOCATION)
    public static boolean registerGnssStatusCallback(@NonNull LocationManager locationManager,
            @NonNull Executor executor, @NonNull GnssStatusCompat.Callback callback) {
        if (VERSION.SDK_INT >= VERSION_CODES.R) {
            return registerGnssStatusCallback(locationManager, null, executor, callback);
        } else {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                looper = Looper.getMainLooper();
            }
            return registerGnssStatusCallback(locationManager, new Handler(looper), executor,
                    callback);
        }
    }

    @RequiresPermission(ACCESS_FINE_LOCATION)
    private static boolean registerGnssStatusCallback(final LocationManager locationManager,
            Handler baseHandler, Executor executor, GnssStatusCompat.Callback callback) {
        if (VERSION.SDK_INT >= 30) {
            return Api30Impl.registerGnssStatusCallback(
                    locationManager, baseHandler, executor, callback);
        } else if (VERSION.SDK_INT >= 24) {
            return Api24Impl.registerGnssStatusCallback(
                    locationManager, baseHandler, executor, callback);
        } else {
            Preconditions.checkArgument(baseHandler != null);
            synchronized (GnssLazyLoader.sGnssStatusListeners) {
                GpsStatusTransport transport =
                        (GpsStatusTransport) GnssLazyLoader.sGnssStatusListeners.get(callback);
                if (transport == null) {
                    transport = new GpsStatusTransport(locationManager, callback);
                } else {
                    transport.unregister();
                }
                transport.register(executor);

                final GpsStatusTransport myTransport = transport;
                FutureTask<Boolean> task = new FutureTask<>(
                        () -> locationManager.addGpsStatusListener(myTransport));

                if (Looper.myLooper() == baseHandler.getLooper()) {
                    task.run();
                } else if (!baseHandler.post(task)) {
                    throw new IllegalStateException(baseHandler + " is shutting down");
                }

                boolean interrupted = false;
                try {
                    long remainingNanos = SECONDS.toNanos(PRE_N_LOOPER_TIMEOUT_S);
                    long end = System.nanoTime() + remainingNanos;
                    while (true) {
                        try {
                            if (task.get(remainingNanos, NANOSECONDS)) {
                                GnssLazyLoader.sGnssStatusListeners.put(callback, myTransport);
                                return true;
                            } else {
                                return false;
                            }
                        } catch (InterruptedException e) {
                            // this is conceptually not an interruptible operation
                            interrupted = true;
                            remainingNanos = end - System.nanoTime();
                        }
                    }
                } catch (ExecutionException e) {
                    if (e.getCause() instanceof RuntimeException) {
                        throw (RuntimeException) e.getCause();
                    } else if (e.getCause() instanceof Error) {
                        throw (Error) e.getCause();
                    } else {
                        throw new IllegalStateException(e);
                    }
                } catch (TimeoutException e) {
                    throw new IllegalStateException(baseHandler + " appears to be blocked, please"
                            + " run registerGnssStatusCallback() directly on a Looper thread or "
                            + "ensure the main Looper is not blocked by this thread", e);
                } finally {
                    if (interrupted) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }
    }

    /**
     * Unregisters a platform agnostic {@link GnssStatusCompat.Callback}. See
     * {@link LocationManager#removeGpsStatusListener(GpsStatus.Listener)}
     * and {@link LocationManager#unregisterGnssStatusCallback(GnssStatus.Callback)}.
     */
    public static void unregisterGnssStatusCallback(@NonNull LocationManager locationManager,
            @NonNull GnssStatusCompat.Callback callback) {
        if (VERSION.SDK_INT >= 24) {
            synchronized (GnssLazyLoader.sGnssStatusListeners) {
                Object transport = GnssLazyLoader.sGnssStatusListeners.remove(callback);
                if (transport != null) {
                    Api24Impl.unregisterGnssStatusCallback(locationManager, transport);
                }
            }
        } else {
            synchronized (GnssLazyLoader.sGnssStatusListeners) {
                GpsStatusTransport transport =
                        (GpsStatusTransport) GnssLazyLoader.sGnssStatusListeners.remove(callback);
                if (transport != null) {
                    transport.unregister();
                    locationManager.removeGpsStatusListener(transport);
                }
            }
        }
    }

    private LocationManagerCompat() {}

    private static class LocationListenerKey {
        final String mProvider;
        final LocationListenerCompat mListener;

        LocationListenerKey(String provider,
                LocationListenerCompat listener) {
            mProvider = ObjectsCompat.requireNonNull(provider, "invalid null provider");
            mListener = ObjectsCompat.requireNonNull(listener, "invalid null listener");
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof LocationListenerKey)) {
                return false;
            }

            LocationListenerKey that = (LocationListenerKey) o;
            return mProvider.equals(that.mProvider) && mListener.equals(that.mListener);
        }

        @Override
        public int hashCode() {
            return ObjectsCompat.hash(mProvider, mListener);
        }
    }

    private static class LocationListenerTransport implements LocationListener {

        @Nullable volatile LocationListenerKey mKey;
        final Executor mExecutor;

        LocationListenerTransport(@Nullable LocationListenerKey key, Executor executor) {
            mKey = key;
            mExecutor = executor;
        }

        public LocationListenerKey getKey() {
            return ObjectsCompat.requireNonNull(mKey);
        }

        public void unregister() {
            mKey = null;
        }

        @Override
        public void onLocationChanged(@NonNull Location location) {
            if (mKey == null) {
                return;
            }

            mExecutor.execute(() -> {
                LocationListenerKey key = mKey;
                if (key == null) {
                    return;
                }
                key.mListener.onLocationChanged(location);
            });
        }

        @Override
        public void onLocationChanged(@NonNull List<Location> locations) {
            if (mKey == null) {
                return;
            }

            mExecutor.execute(() -> {
                LocationListenerKey key = mKey;
                if (key == null) {
                    return;
                }
                key.mListener.onLocationChanged(locations);
            });
        }

        @Override
        public void onFlushComplete(int requestCode) {
            if (mKey == null) {
                return;
            }

            mExecutor.execute(() -> {
                LocationListenerKey key = mKey;
                if (key == null) {
                    return;
                }
                key.mListener.onFlushComplete(requestCode);
            });
        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
            if (mKey == null) {
                return;
            }

            mExecutor.execute(() -> {
                LocationListenerKey key = mKey;
                if (key == null) {
                    return;
                }
                key.mListener.onStatusChanged(provider, status, extras);
            });
        }

        @Override
        public void onProviderEnabled(@NonNull String provider) {
            if (mKey == null) {
                return;
            }

            mExecutor.execute(() -> {
                LocationListenerKey key = mKey;
                if (key == null) {
                    return;
                }
                key.mListener.onProviderEnabled(provider);
            });
        }

        @Override
        public void onProviderDisabled(@NonNull String provider) {
            if (mKey == null) {
                return;
            }

            mExecutor.execute(() -> {
                LocationListenerKey key = mKey;
                if (key == null) {
                    return;
                }
                key.mListener.onProviderDisabled(provider);
            });
        }
    }

    @RequiresApi(30)
    private static class GnssStatusTransport extends GnssStatus.Callback {

        final GnssStatusCompat.Callback mCallback;

        GnssStatusTransport(GnssStatusCompat.Callback callback) {
            Preconditions.checkArgument(callback != null, "invalid null callback");
            mCallback = callback;
        }

        @Override
        public void onStarted() {
            mCallback.onStarted();
        }

        @Override
        public void onStopped() {
            mCallback.onStopped();
        }

        @Override
        public void onFirstFix(int ttffMillis) {
            mCallback.onFirstFix(ttffMillis);
        }

        @Override
        public void onSatelliteStatusChanged(GnssStatus status) {
            mCallback.onSatelliteStatusChanged(GnssStatusCompat.wrap(status));
        }
    }

    @RequiresApi(24)
    private static class PreRGnssStatusTransport extends GnssStatus.Callback {

        final GnssStatusCompat.Callback mCallback;

        @Nullable volatile Executor mExecutor;

        PreRGnssStatusTransport(GnssStatusCompat.Callback callback) {
            Preconditions.checkArgument(callback != null, "invalid null callback");
            mCallback = callback;
        }

        public void register(Executor executor) {
            Preconditions.checkArgument(executor != null, "invalid null executor");
            Preconditions.checkState(mExecutor == null);
            mExecutor = executor;
        }

        public void unregister() {
            mExecutor = null;
        }

        @Override
        public void onStarted() {
            final Executor executor = mExecutor;
            if (executor == null) {
                return;
            }

            executor.execute(() -> {
                if (mExecutor != executor) {
                    return;
                }
                mCallback.onStarted();
            });
        }

        @Override
        public void onStopped() {
            final Executor executor = mExecutor;
            if (executor == null) {
                return;
            }

            executor.execute(() -> {
                if (mExecutor != executor) {
                    return;
                }
                mCallback.onStopped();
            });
        }

        @Override
        public void onFirstFix(final int ttffMillis) {
            final Executor executor = mExecutor;
            if (executor == null) {
                return;
            }

            executor.execute(() -> {
                if (mExecutor != executor) {
                    return;
                }
                mCallback.onFirstFix(ttffMillis);
            });
        }

        @Override
        public void onSatelliteStatusChanged(final GnssStatus status) {
            final Executor executor = mExecutor;
            if (executor == null) {
                return;
            }

            executor.execute(() -> {
                if (mExecutor != executor) {
                    return;
                }
                mCallback.onSatelliteStatusChanged(GnssStatusCompat.wrap(status));
            });
        }
    }

    private static class GpsStatusTransport implements GpsStatus.Listener {

        private final LocationManager mLocationManager;
        final GnssStatusCompat.Callback mCallback;

        @Nullable volatile Executor mExecutor;

        GpsStatusTransport(LocationManager locationManager,
                GnssStatusCompat.Callback callback) {
            Preconditions.checkArgument(callback != null, "invalid null callback");
            mLocationManager = locationManager;
            mCallback = callback;
        }

        public void register(Executor executor) {
            Preconditions.checkState(mExecutor == null);
            mExecutor = executor;
        }

        public void unregister() {
            mExecutor = null;
        }

        @RequiresPermission(ACCESS_FINE_LOCATION)
        @Override
        public void onGpsStatusChanged(int event) {
            final Executor executor = mExecutor;
            if (executor == null) {
                return;
            }

            GpsStatus gpsStatus;

            switch (event) {
                case GpsStatus.GPS_EVENT_STARTED:
                    executor.execute(() -> {
                        if (mExecutor != executor) {
                            return;
                        }
                        mCallback.onStarted();
                    });
                    break;
                case GpsStatus.GPS_EVENT_STOPPED:
                    executor.execute(() -> {
                        if (mExecutor != executor) {
                            return;
                        }
                        mCallback.onStopped();
                    });
                    break;
                case GpsStatus.GPS_EVENT_FIRST_FIX:
                    gpsStatus = mLocationManager.getGpsStatus(null);
                    if (gpsStatus != null) {
                        final int ttff = gpsStatus.getTimeToFirstFix();
                        executor.execute(() -> {
                            if (mExecutor != executor) {
                                return;
                            }
                            mCallback.onFirstFix(ttff);
                        });
                    }
                    break;
                case GpsStatus.GPS_EVENT_SATELLITE_STATUS:
                    gpsStatus = mLocationManager.getGpsStatus(null);
                    if (gpsStatus != null) {
                        final GnssStatusCompat gnssStatus = GnssStatusCompat.wrap(gpsStatus);
                        executor.execute(() -> {
                            if (mExecutor != executor) {
                                return;
                            }
                            mCallback.onSatelliteStatusChanged(gnssStatus);
                        });
                    }
                    break;
            }
        }
    }

    private static final class CancellableLocationListener implements LocationListener {

        private final LocationManager mLocationManager;
        private final Executor mExecutor;
        private final Handler mTimeoutHandler;

        private Consumer<Location> mConsumer;

        @GuardedBy("this")
        private boolean mTriggered;

        @Nullable
        Runnable mTimeoutRunnable;

        CancellableLocationListener(LocationManager locationManager,
                Executor executor, Consumer<Location> consumer) {
            mLocationManager = locationManager;
            mExecutor = executor;
            mTimeoutHandler = new Handler(Looper.getMainLooper());

            mConsumer = consumer;
        }

        @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
        public void cancel() {
            synchronized (this) {
                if (mTriggered) {
                    return;
                }
                mTriggered = true;
            }

            cleanup();
        }

        @SuppressLint("MissingPermission") // Can't annotate a lambda
        public void startTimeout(long timeoutMs) {
            synchronized (this) {
                if (mTriggered) {
                    return;
                }

                // ideally this would be a wakeup alarm, but that would require another compat layer
                // to deal with translating pending intent alarms into listeners which doesn't exist
                // at the moment, so this should be sufficient to prevent extreme battery drain
                mTimeoutRunnable = () -> {
                    mTimeoutRunnable = null;
                    onLocationChanged((Location) null);
                };
                mTimeoutHandler.postDelayed(mTimeoutRunnable, timeoutMs);
            }
        }

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

        @Override
        public void onProviderEnabled(@NonNull String provider) {}

        @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
        @Override
        public void onProviderDisabled(@NonNull String p) {
            onLocationChanged((Location) null);
        }

        @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
        @Override
        public void onLocationChanged(@Nullable final Location location) {
            synchronized (this) {
                if (mTriggered) {
                    return;
                }
                mTriggered = true;
            }

            final Consumer<Location> consumer = mConsumer;
            mExecutor.execute(() -> consumer.accept(location));

            cleanup();
        }

        @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
        private void cleanup() {
            mConsumer = null;
            mLocationManager.removeUpdates(this);
            if (mTimeoutRunnable != null) {
                mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
                mTimeoutRunnable = null;
            }
        }
    }


    /**
     * An {@link Executor} that posts all executed tasks onto the given {@link Handler}. This
     * version differs from {@link ExecutorCompat#create(Handler)} in that if the execute call is
     * already occurring on the Looper of the given Handler, the Runnable will simply be executed
     * directly. This avoids the cost of an additional thread trampoline when not necessary, but
     * can introduce out-of-order execution violations as it is possible a given Runnable may
     * execute before some other Runnable that was submitted to the executor earlier. Because of
     * this limitation, use this Executor only when you are sure that all Runnables will always
     * be submitted to this Executor from the same logical thread, and only if it is acceptable to
     * bypass the given Handler completely.
     */
    private static final class InlineHandlerExecutor implements Executor {
        private final Handler mHandler;

        InlineHandlerExecutor(@NonNull Handler handler) {
            mHandler = Preconditions.checkNotNull(handler);
        }

        @Override
        public void execute(@NonNull Runnable command) {
            if (Looper.myLooper() == mHandler.getLooper()) {
                command.run();
            } else if (!mHandler.post(Preconditions.checkNotNull(command))) {
                throw new RejectedExecutionException(mHandler + " is shutting down");
            }
        }
    }

    @RequiresApi(31)
    private static class Api31Impl {
        private Api31Impl() {
            // This class is not instantiable.
        }

        @DoNotInline
        static boolean hasProvider(LocationManager locationManager, @NonNull String provider) {
            return locationManager.hasProvider(provider);
        }

        @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
        @DoNotInline
        static void requestLocationUpdates(LocationManager locationManager,
                @NonNull String provider, @NonNull LocationRequest locationRequest,
                @NonNull Executor executor, @NonNull LocationListener listener) {
            locationManager.requestLocationUpdates(provider, locationRequest, executor, listener);
        }
    }

    @RequiresApi(30)
    private static class Api30Impl {
        private static Class<?> sLocationRequestClass;
        private static Method sRequestLocationUpdatesExecutorMethod;

        private Api30Impl() {
            // This class is not instantiable.
        }

        @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
        @DoNotInline
        static void getCurrentLocation(LocationManager locationManager, @NonNull String provider,
                @Nullable CancellationSignal cancellationSignal,
                @NonNull Executor executor, final @NonNull Consumer<Location> consumer) {
            locationManager.getCurrentLocation(provider,
                    cancellationSignal != null
                            ? (android.os.CancellationSignal)
                            cancellationSignal.getCancellationSignalObject()
                            : null,
                    executor,
                    consumer::accept);
        }

        @SuppressWarnings("JavaReflectionMemberAccess")
        @DoNotInline
        public static boolean tryRequestLocationUpdates(LocationManager locationManager,
                String provider, LocationRequestCompat locationRequest, Executor executor,
                LocationListenerCompat listener) {
            if (VERSION.SDK_INT >= 30) { // Satisfy reflection lint check
                try {
                    if (sLocationRequestClass == null) {
                        sLocationRequestClass = Class.forName("android.location.LocationRequest");
                    }
                    if (sRequestLocationUpdatesExecutorMethod == null) {
                        sRequestLocationUpdatesExecutorMethod =
                                LocationManager.class.getDeclaredMethod(
                                        "requestLocationUpdates",
                                        sLocationRequestClass, Executor.class,
                                        LocationListener.class);
                        sRequestLocationUpdatesExecutorMethod.setAccessible(true);
                    }

                    Object request = locationRequest.toLocationRequest(provider);
                    if (request != null) {
                        sRequestLocationUpdatesExecutorMethod.invoke(locationManager, request,
                                executor,
                                listener);
                        return true;
                    }
                } catch (NoSuchMethodException
                        | InvocationTargetException
                        | IllegalAccessException
                        | ClassNotFoundException
                        | UnsupportedOperationException e) {
                    // ignored
                }
            }
            return false;
        }

        @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
        @DoNotInline
        public static boolean registerGnssStatusCallback(LocationManager locationManager,
                Handler baseHandler, Executor executor, GnssStatusCompat.Callback callback) {
            synchronized (GnssLazyLoader.sGnssStatusListeners) {
                GnssStatusTransport transport =
                        (GnssStatusTransport) GnssLazyLoader.sGnssStatusListeners.get(callback);
                if (transport == null) {
                    transport = new GnssStatusTransport(callback);
                }
                if (locationManager.registerGnssStatusCallback(executor, transport)) {
                    GnssLazyLoader.sGnssStatusListeners.put(callback, transport);
                    return true;
                } else {
                    return false;
                }
            }
        }
    }

    @RequiresApi(28)
    private static class Api28Impl {
        private Api28Impl() {
            // This class is not instantiable.
        }

        @DoNotInline
        static boolean isLocationEnabled(LocationManager locationManager) {
            return locationManager.isLocationEnabled();
        }

        @DoNotInline
        static String getGnssHardwareModelName(LocationManager locationManager) {
            return locationManager.getGnssHardwareModelName();
        }

        @DoNotInline
        static int getGnssYearOfHardware(LocationManager locationManager) {
            return locationManager.getGnssYearOfHardware();
        }
    }

    @RequiresApi(19)
    static class Api19Impl {
        private static Class<?> sLocationRequestClass;
        private static Method sRequestLocationUpdatesLooperMethod;

        private Api19Impl() {
            // This class is not instantiable.
        }

        @SuppressWarnings("JavaReflectionMemberAccess")
        @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
        @DoNotInline
        static boolean tryRequestLocationUpdates(LocationManager locationManager,
                String provider, LocationRequestCompat locationRequest,
                LocationListenerTransport transport) {
            if (VERSION.SDK_INT >= 19) { // Satisfy reflection lint check
                try {
                    if (sLocationRequestClass == null) {
                        sLocationRequestClass = Class.forName("android.location.LocationRequest");
                    }
                    if (sRequestLocationUpdatesLooperMethod == null) {
                        sRequestLocationUpdatesLooperMethod =
                                LocationManager.class.getDeclaredMethod(
                                        "requestLocationUpdates",
                                        sLocationRequestClass, LocationListener.class,
                                        Looper.class);
                        sRequestLocationUpdatesLooperMethod.setAccessible(true);
                    }

                    LocationRequest request = locationRequest.toLocationRequest(provider);
                    if (request != null) {
                        synchronized (sLocationListeners) {
                            sRequestLocationUpdatesLooperMethod.invoke(locationManager, request,
                                    transport, Looper.getMainLooper());
                            registerLocationListenerTransport(locationManager, transport);
                            return true;
                        }
                    }
                } catch (NoSuchMethodException
                        | InvocationTargetException
                        | IllegalAccessException
                        | ClassNotFoundException
                        | UnsupportedOperationException e) {
                    // ignored
                }
            }
            return false;
        }

        @SuppressWarnings("JavaReflectionMemberAccess")
        @DoNotInline
        static boolean tryRequestLocationUpdates(LocationManager locationManager, String provider,
                LocationRequestCompat locationRequest, LocationListenerCompat listener,
                Looper looper) {
            if (VERSION.SDK_INT >= 19) { // Satisfy reflection lint check
                try {
                    if (sLocationRequestClass == null) {
                        sLocationRequestClass = Class.forName("android.location.LocationRequest");
                    }

                    if (sRequestLocationUpdatesLooperMethod == null) {
                        sRequestLocationUpdatesLooperMethod =
                                LocationManager.class.getDeclaredMethod(
                                        "requestLocationUpdates",
                                        sLocationRequestClass, LocationListener.class,
                                        Looper.class);
                        sRequestLocationUpdatesLooperMethod.setAccessible(true);
                    }

                    LocationRequest request = locationRequest.toLocationRequest(provider);
                    if (request != null) {
                        sRequestLocationUpdatesLooperMethod.invoke(
                                locationManager, request, listener, looper);
                        return true;
                    }
                } catch (NoSuchMethodException
                        | InvocationTargetException
                        | IllegalAccessException
                        | ClassNotFoundException
                        | UnsupportedOperationException e) {
                    // ignored
                }
            }
            return false;
        }
    }

    @RequiresApi(24)
    static class Api24Impl {
        private Api24Impl() {
            // This class is not instantiable.
        }

        @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
        @DoNotInline
        static boolean registerGnssStatusCallback(LocationManager locationManager,
                Handler baseHandler, Executor executor, GnssStatusCompat.Callback callback) {
            Preconditions.checkArgument(baseHandler != null);

            synchronized (GnssLazyLoader.sGnssStatusListeners) {
                PreRGnssStatusTransport transport =
                        (PreRGnssStatusTransport) GnssLazyLoader.sGnssStatusListeners.get(callback);
                if (transport == null) {
                    transport = new PreRGnssStatusTransport(callback);
                } else {
                    transport.unregister();
                }
                transport.register(executor);

                if (locationManager.registerGnssStatusCallback(transport, baseHandler)) {
                    GnssLazyLoader.sGnssStatusListeners.put(callback, transport);
                    return true;
                } else {
                    return false;
                }
            }
        }

        @DoNotInline
        static void unregisterGnssStatusCallback(LocationManager locationManager, Object callback) {
            if (callback instanceof PreRGnssStatusTransport) {
                ((PreRGnssStatusTransport) callback).unregister();
            }
            locationManager.unregisterGnssStatusCallback((GnssStatus.Callback) callback);
        }
    }
}