All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowLocationManager Maven / Gradle / Ivy

package org.robolectric.shadows;

import static android.location.LocationManager.GPS_PROVIDER;
import static android.location.LocationManager.NETWORK_PROVIDER;
import static android.location.LocationManager.PASSIVE_PROVIDER;
import static android.os.Build.VERSION_CODES.P;
import static android.provider.Settings.Secure.LOCATION_MODE;
import static android.provider.Settings.Secure.LOCATION_MODE_BATTERY_SAVING;
import static android.provider.Settings.Secure.LOCATION_MODE_HIGH_ACCURACY;
import static android.provider.Settings.Secure.LOCATION_MODE_OFF;
import static android.provider.Settings.Secure.LOCATION_MODE_SENSORS_ONLY;
import static android.provider.Settings.Secure.LOCATION_PROVIDERS_ALLOWED;
import static java.util.concurrent.TimeUnit.NANOSECONDS;

import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Context;
import android.content.Intent;
import android.location.Criteria;
import android.location.GnssAntennaInfo;
import android.location.GnssMeasurementsEvent;
import android.location.GnssStatus;
import android.location.GpsStatus;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.location.LocationRequest;
import android.location.OnNmeaMessageListener;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.WorkSource;
import android.provider.Settings.Secure;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.function.Consumer;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadows.ShadowSettings.ShadowSecure;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;

/**
 * Shadow for {@link LocationManager}. Note that the default state of location on Android devices is
 * location on, gps provider enabled, network provider disabled.
 */
@SuppressWarnings("deprecation")
@Implements(value = LocationManager.class, looseSignatures = true)
public class ShadowLocationManager {

  private static final String TAG = "ShadowLocationManager";

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

  /**
   * ProviderProperties is not public prior to S, so a new class is required to represent it prior
   * to that platform.
   */
  public static class ProviderProperties {
    @Nullable private final Object properties;

    private final boolean requiresNetwork;
    private final boolean requiresSatellite;
    private final boolean requiresCell;
    private final boolean hasMonetaryCost;
    private final boolean supportsAltitude;
    private final boolean supportsSpeed;
    private final boolean supportsBearing;
    private final int powerRequirement;
    private final int accuracy;

    @RequiresApi(VERSION_CODES.S)
    ProviderProperties(android.location.provider.ProviderProperties properties) {
      this.properties = Objects.requireNonNull(properties);
      this.requiresNetwork = false;
      this.requiresSatellite = false;
      this.requiresCell = false;
      this.hasMonetaryCost = false;
      this.supportsAltitude = false;
      this.supportsSpeed = false;
      this.supportsBearing = false;
      this.powerRequirement = 0;
      this.accuracy = 0;
    }

    public ProviderProperties(
        boolean requiresNetwork,
        boolean requiresSatellite,
        boolean requiresCell,
        boolean hasMonetaryCost,
        boolean supportsAltitude,
        boolean supportsSpeed,
        boolean supportsBearing,
        int powerRequirement,
        int accuracy) {
      if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.S) {
        properties =
            new android.location.provider.ProviderProperties.Builder()
                .setHasNetworkRequirement(requiresNetwork)
                .setHasSatelliteRequirement(requiresSatellite)
                .setHasCellRequirement(requiresCell)
                .setHasMonetaryCost(hasMonetaryCost)
                .setHasAltitudeSupport(supportsAltitude)
                .setHasSpeedSupport(supportsSpeed)
                .setHasBearingSupport(supportsBearing)
                .setPowerUsage(powerRequirement)
                .setAccuracy(accuracy)
                .build();
      } else {
        properties = null;
      }

      this.requiresNetwork = requiresNetwork;
      this.requiresSatellite = requiresSatellite;
      this.requiresCell = requiresCell;
      this.hasMonetaryCost = hasMonetaryCost;
      this.supportsAltitude = supportsAltitude;
      this.supportsSpeed = supportsSpeed;
      this.supportsBearing = supportsBearing;
      this.powerRequirement = powerRequirement;
      this.accuracy = accuracy;
    }

    public ProviderProperties(Criteria criteria) {
      this(
          false,
          false,
          false,
          criteria.isCostAllowed(),
          criteria.isAltitudeRequired(),
          criteria.isSpeedRequired(),
          criteria.isBearingRequired(),
          criteria.getPowerRequirement(),
          criteria.getAccuracy());
    }

    @RequiresApi(VERSION_CODES.S)
    android.location.provider.ProviderProperties getProviderProperties() {
      return (android.location.provider.ProviderProperties) Objects.requireNonNull(properties);
    }

    Object getLegacyProviderProperties() {
      try {
        return ReflectionHelpers.callConstructor(
            Class.forName("com.android.internal.location.ProviderProperties"),
            ClassParameter.from(boolean.class, requiresNetwork),
            ClassParameter.from(boolean.class, requiresSatellite),
            ClassParameter.from(boolean.class, requiresCell),
            ClassParameter.from(boolean.class, hasMonetaryCost),
            ClassParameter.from(boolean.class, supportsAltitude),
            ClassParameter.from(boolean.class, supportsSpeed),
            ClassParameter.from(boolean.class, supportsBearing),
            ClassParameter.from(int.class, powerRequirement),
            ClassParameter.from(int.class, accuracy));
      } catch (ClassNotFoundException c) {
        throw new RuntimeException("Unable to load old ProviderProperties class", c);
      }
    }

    public boolean hasNetworkRequirement() {
      if (properties != null) {
        return ((android.location.provider.ProviderProperties) properties).hasNetworkRequirement();
      } else {
        return requiresNetwork;
      }
    }

    public boolean hasSatelliteRequirement() {
      if (properties != null) {
        return ((android.location.provider.ProviderProperties) properties)
            .hasSatelliteRequirement();
      } else {
        return requiresSatellite;
      }
    }

    public boolean isRequiresCell() {
      if (properties != null) {
        return ((android.location.provider.ProviderProperties) properties).hasCellRequirement();
      } else {
        return requiresCell;
      }
    }

    public boolean isHasMonetaryCost() {
      if (properties != null) {
        return ((android.location.provider.ProviderProperties) properties).hasMonetaryCost();
      } else {
        return hasMonetaryCost;
      }
    }

    public boolean hasAltitudeSupport() {
      if (properties != null) {
        return ((android.location.provider.ProviderProperties) properties).hasAltitudeSupport();
      } else {
        return supportsAltitude;
      }
    }

    public boolean hasSpeedSupport() {
      if (properties != null) {
        return ((android.location.provider.ProviderProperties) properties).hasSpeedSupport();
      } else {
        return supportsSpeed;
      }
    }

    public boolean hasBearingSupport() {
      if (properties != null) {
        return ((android.location.provider.ProviderProperties) properties).hasBearingSupport();
      } else {
        return supportsBearing;
      }
    }

    public int getPowerUsage() {
      if (properties != null) {
        return ((android.location.provider.ProviderProperties) properties).getPowerUsage();
      } else {
        return powerRequirement;
      }
    }

    public int getAccuracy() {
      if (properties != null) {
        return ((android.location.provider.ProviderProperties) properties).getAccuracy();
      } else {
        return accuracy;
      }
    }

    boolean meetsCriteria(Criteria criteria) {
      if (criteria.getAccuracy() != Criteria.NO_REQUIREMENT
          && criteria.getAccuracy() < getAccuracy()) {
        return false;
      }
      if (criteria.getPowerRequirement() != Criteria.NO_REQUIREMENT
          && criteria.getPowerRequirement() < getPowerUsage()) {
        return false;
      }
      if (criteria.isAltitudeRequired() && !hasAltitudeSupport()) {
        return false;
      }
      if (criteria.isSpeedRequired() && !hasSpeedSupport()) {
        return false;
      }
      if (criteria.isBearingRequired() && !hasBearingSupport()) {
        return false;
      }
      if (!criteria.isCostAllowed() && hasMonetaryCost) {
        return false;
      }
      return true;
    }
  }

  @GuardedBy("ShadowLocationManager.class")
  @Nullable
  private static Constructor locationProviderConstructor;

  @RealObject private LocationManager realLocationManager;

  @GuardedBy("providers")
  private final HashSet providers = new HashSet<>();

  @GuardedBy("gpsStatusListeners")
  private final HashSet gpsStatusListeners = new HashSet<>();

  @GuardedBy("gnssStatusTransports")
  private final CopyOnWriteArrayList gnssStatusTransports =
      new CopyOnWriteArrayList<>();

  @GuardedBy("nmeaMessageTransports")
  private final CopyOnWriteArrayList nmeaMessageTransports =
      new CopyOnWriteArrayList<>();

  @GuardedBy("gnssMeasurementTransports")
  private final CopyOnWriteArrayList
      gnssMeasurementTransports = new CopyOnWriteArrayList<>();

  @GuardedBy("gnssAntennaInfoTransports")
  private final CopyOnWriteArrayList gnssAntennaInfoTransports =
      new CopyOnWriteArrayList<>();

  @Nullable private String gnssHardwareModelName;

  private int gnssYearOfHardware;

  private int gnssBatchSize;

  public ShadowLocationManager() {
    // create default providers
    providers.add(
        new ProviderEntry(
            GPS_PROVIDER,
            new ProviderProperties(
                true,
                true,
                false,
                false,
                true,
                true,
                true,
                Criteria.POWER_HIGH,
                Criteria.ACCURACY_FINE)));
    providers.add(
        new ProviderEntry(
            NETWORK_PROVIDER,
            new ProviderProperties(
                false,
                false,
                false,
                false,
                true,
                true,
                true,
                Criteria.POWER_LOW,
                Criteria.ACCURACY_COARSE)));
    providers.add(
        new ProviderEntry(
            PASSIVE_PROVIDER,
            new ProviderProperties(
                false,
                false,
                false,
                false,
                false,
                false,
                false,
                Criteria.POWER_LOW,
                Criteria.ACCURACY_COARSE)));
  }

  @Implementation
  protected List getAllProviders() {
    ArrayList allProviders = new ArrayList<>();
    for (ProviderEntry providerEntry : getProviderEntries()) {
      allProviders.add(providerEntry.getName());
    }
    return allProviders;
  }

  @Implementation
  @Nullable
  protected LocationProvider getProvider(String name) {
    if (RuntimeEnvironment.getApiLevel() < VERSION_CODES.KITKAT) {
      // jelly bean has no way to properly construct a LocationProvider, we give up
      return null;
    }

    ProviderEntry providerEntry = getProviderEntry(name);
    if (providerEntry == null) {
      return null;
    }

    ProviderProperties properties = providerEntry.getProperties();
    if (properties == null) {
      return null;
    }

    try {
      synchronized (ShadowLocationManager.class) {
        if (locationProviderConstructor == null) {
          if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.S) {
            locationProviderConstructor =
                LocationProvider.class.getDeclaredConstructor(
                    String.class, android.location.provider.ProviderProperties.class);
          } else {
            locationProviderConstructor =
                LocationProvider.class.getDeclaredConstructor(
                    String.class,
                    Class.forName("com.android.internal.location.ProviderProperties"));
          }
        }
        locationProviderConstructor.setAccessible(true);

        if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.S) {
          return locationProviderConstructor.newInstance(name, properties.getProviderProperties());
        } else {
          return locationProviderConstructor.newInstance(
              name, properties.getLegacyProviderProperties());
        }
      }
    } catch (ReflectiveOperationException e) {
      throw new LinkageError(e.getMessage(), e);
    }
  }

  @Implementation
  protected List getProviders(boolean enabledOnly) {
    return getProviders(null, enabledOnly);
  }

  @Implementation
  protected List getProviders(@Nullable Criteria criteria, boolean enabled) {
    ArrayList matchingProviders = new ArrayList<>();
    for (ProviderEntry providerEntry : getProviderEntries()) {
      if (enabled && !isProviderEnabled(providerEntry.getName())) {
        continue;
      }
      if (criteria != null && !providerEntry.meetsCriteria(criteria)) {
        continue;
      }
      matchingProviders.add(providerEntry.getName());
    }
    return matchingProviders;
  }

  @Implementation
  @Nullable
  protected String getBestProvider(Criteria criteria, boolean enabled) {
    List providers = getProviders(criteria, enabled);
    if (providers.isEmpty()) {
      providers = getProviders(null, enabled);
    }

    if (!providers.isEmpty()) {
      if (providers.contains(GPS_PROVIDER)) {
        return GPS_PROVIDER;
      } else if (providers.contains(NETWORK_PROVIDER)) {
        return NETWORK_PROVIDER;
      } else {
        return providers.get(0);
      }
    }

    return null;
  }

  @Implementation(minSdk = VERSION_CODES.S)
  @Nullable
  protected Object getProviderProperties(Object providerStr) {
    String provider = (String) providerStr;
    if (provider == null) {
      throw new IllegalArgumentException();
    }

    ProviderEntry providerEntry = getProviderEntry(provider);
    if (providerEntry == null) {
      return null;
    }

    ProviderProperties properties = providerEntry.getProperties();
    if (properties == null) {
      return null;
    }

    return properties.getProviderProperties();
  }

  @Implementation(minSdk = VERSION_CODES.S)
  protected boolean hasProvider(String provider) {
    if (provider == null) {
      throw new IllegalArgumentException();
    }

    return getProviderEntry(provider) != null;
  }

  @Implementation
  protected boolean isProviderEnabled(String provider) {
    if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.P) {
      if (!isLocationEnabled()) {
        return false;
      }
    }

    ProviderEntry entry = getProviderEntry(provider);
    return entry != null && entry.isEnabled();
  }

  /** Completely removes a provider. */
  public void removeProvider(String name) {
    removeProviderEntry(name);
  }

  /**
   * Sets the properties of the given provider. The provider will be created if it doesn't exist
   * already. This overload functions for all Android SDK levels.
   */
  public void setProviderProperties(String name, @Nullable ProviderProperties properties) {
    getOrCreateProviderEntry(Objects.requireNonNull(name)).setProperties(properties);
  }

  /**
   * Sets the given provider enabled or disabled. The provider will be created if it doesn't exist
   * already. On P and above, location must also be enabled via {@link #setLocationEnabled(boolean)}
   * in order for a provider to be considered enabled.
   */
  public void setProviderEnabled(String name, boolean enabled) {
    getOrCreateProviderEntry(name).setEnabled(enabled);
  }

  // @SystemApi
  @Implementation(minSdk = VERSION_CODES.P)
  protected boolean isLocationEnabledForUser(UserHandle userHandle) {
    return isLocationEnabled();
  }

  @Implementation(minSdk = P)
  protected boolean isLocationEnabled() {
    return getLocationMode() != LOCATION_MODE_OFF;
  }

  // @SystemApi
  @Implementation(minSdk = VERSION_CODES.P)
  protected void setLocationEnabledForUser(boolean enabled, UserHandle userHandle) {
    setLocationModeInternal(enabled ? LOCATION_MODE_HIGH_ACCURACY : LOCATION_MODE_OFF);
  }

  /**
   * On P and above, turns location on or off. On pre-P devices, sets the location mode to {@link
   * android.provider.Settings.Secure#LOCATION_MODE_HIGH_ACCURACY} or {@link
   * android.provider.Settings.Secure#LOCATION_MODE_OFF}.
   */
  public void setLocationEnabled(boolean enabled) {
    setLocationEnabledForUser(enabled, Process.myUserHandle());
  }

  private int getLocationMode() {
    return Secure.getInt(getContext().getContentResolver(), LOCATION_MODE, LOCATION_MODE_OFF);
  }

  /**
   * On pre-P devices, sets the device location mode. For P and above, use {@link
   * #setLocationEnabled(boolean)} and {@link #setProviderEnabled(String, boolean)} in combination
   * to achieve the desired effect.
   */
  public void setLocationMode(int locationMode) {
    if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.P) {
      throw new AssertionError(
          "Tests may not set location mode directly on P and above. Instead, use"
              + " setLocationEnabled() and setProviderEnabled() in combination to achieve the"
              + " desired result.");
    }

    setLocationModeInternal(locationMode);
  }

  private void setLocationModeInternal(int locationMode) {
    Secure.putInt(getContext().getContentResolver(), LOCATION_MODE, locationMode);
  }

  @Implementation
  @Nullable
  protected Location getLastKnownLocation(String provider) {
    ProviderEntry providerEntry = getProviderEntry(provider);
    if (providerEntry == null) {
      return null;
    }

    return providerEntry.getLastLocation();
  }

  /**
   * @deprecated Use {@link #simulateLocation(Location)} to update the last location for a provider.
   */
  @Deprecated
  public void setLastKnownLocation(String provider, @Nullable Location location) {
    getOrCreateProviderEntry(provider).setLastLocation(location);
  }

  @RequiresApi(api = VERSION_CODES.R)
  @Implementation(minSdk = VERSION_CODES.R)
  protected void getCurrentLocation(
      String provider,
      @Nullable CancellationSignal cancellationSignal,
      Executor executor,
      Consumer consumer) {
    getCurrentLocationInternal(
        provider, LocationRequest.create(), cancellationSignal, executor, consumer);
  }

  @RequiresApi(api = VERSION_CODES.S)
  @Implementation(minSdk = VERSION_CODES.S)
  protected void getCurrentLocation(
      String provider,
      LocationRequest request,
      @Nullable CancellationSignal cancellationSignal,
      Executor executor,
      Consumer consumer) {
    getCurrentLocationInternal(provider, request, cancellationSignal, executor, consumer);
  }

  @RequiresApi(api = VERSION_CODES.R)
  private void getCurrentLocationInternal(
      String provider,
      LocationRequest request,
      @Nullable CancellationSignal cancellationSignal,
      Executor executor,
      Consumer consumer) {
    if (cancellationSignal != null) {
      cancellationSignal.throwIfCanceled();
    }

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

    CurrentLocationTransport listener = new CurrentLocationTransport(executor, consumer);
    requestLocationUpdatesInternal(
        provider, new RoboLocationRequest(request), Runnable::run, listener);

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

    listener.startTimeout(GET_CURRENT_LOCATION_TIMEOUT_MS);
  }

  @Implementation
  protected void requestSingleUpdate(
      String provider, LocationListener listener, @Nullable Looper looper) {
    if (looper == null) {
      looper = Looper.myLooper();
      if (looper == null) {
        // forces appropriate exception
        new Handler();
      }
    }
    requestLocationUpdatesInternal(
        provider,
        new RoboLocationRequest(provider, 0, 0, true),
        new HandlerExecutor(new Handler(looper)),
        listener);
  }

  @Implementation
  protected void requestSingleUpdate(
      Criteria criteria, LocationListener listener, @Nullable Looper looper) {
    String bestProvider = getBestProvider(criteria, true);
    if (bestProvider == null) {
      throw new IllegalArgumentException("no providers found for criteria");
    }
    if (looper == null) {
      looper = Looper.myLooper();
      if (looper == null) {
        // forces appropriate exception
        new Handler();
      }
    }
    requestLocationUpdatesInternal(
        bestProvider,
        new RoboLocationRequest(bestProvider, 0, 0, true),
        new HandlerExecutor(new Handler(looper)),
        listener);
  }

  @Implementation
  protected void requestSingleUpdate(String provider, PendingIntent pendingIntent) {
    requestLocationUpdatesInternal(
        provider, new RoboLocationRequest(provider, 0, 0, true), pendingIntent);
  }

  @Implementation
  protected void requestSingleUpdate(Criteria criteria, PendingIntent pendingIntent) {
    String bestProvider = getBestProvider(criteria, true);
    if (bestProvider == null) {
      throw new IllegalArgumentException("no providers found for criteria");
    }
    requestLocationUpdatesInternal(
        bestProvider, new RoboLocationRequest(bestProvider, 0, 0, true), pendingIntent);
  }

  @Implementation
  protected void requestLocationUpdates(
      String provider, long minTime, float minDistance, LocationListener listener) {
    requestLocationUpdatesInternal(
        provider,
        new RoboLocationRequest(provider, minTime, minDistance, false),
        new HandlerExecutor(new Handler()),
        listener);
  }

  @Implementation
  protected void requestLocationUpdates(
      String provider,
      long minTime,
      float minDistance,
      LocationListener listener,
      @Nullable Looper looper) {
    if (looper == null) {
      looper = Looper.myLooper();
      if (looper == null) {
        // forces appropriate exception
        new Handler();
      }
    }
    requestLocationUpdatesInternal(
        provider,
        new RoboLocationRequest(provider, minTime, minDistance, false),
        new HandlerExecutor(new Handler(looper)),
        listener);
  }

  @Implementation(minSdk = VERSION_CODES.R)
  protected void requestLocationUpdates(
      String provider,
      long minTime,
      float minDistance,
      Executor executor,
      LocationListener listener) {
    requestLocationUpdatesInternal(
        provider,
        new RoboLocationRequest(provider, minTime, minDistance, false),
        executor,
        listener);
  }

  @Implementation
  protected void requestLocationUpdates(
      long minTime,
      float minDistance,
      Criteria criteria,
      LocationListener listener,
      @Nullable Looper looper) {
    String bestProvider = getBestProvider(criteria, true);
    if (bestProvider == null) {
      throw new IllegalArgumentException("no providers found for criteria");
    }
    if (looper == null) {
      looper = Looper.myLooper();
      if (looper == null) {
        // forces appropriate exception
        new Handler();
      }
    }
    requestLocationUpdatesInternal(
        bestProvider,
        new RoboLocationRequest(bestProvider, minTime, minDistance, false),
        new HandlerExecutor(new Handler(looper)),
        listener);
  }

  @Implementation(minSdk = VERSION_CODES.R)
  protected void requestLocationUpdates(
      long minTime,
      float minDistance,
      Criteria criteria,
      Executor executor,
      LocationListener listener) {
    String bestProvider = getBestProvider(criteria, true);
    if (bestProvider == null) {
      throw new IllegalArgumentException("no providers found for criteria");
    }
    requestLocationUpdatesInternal(
        bestProvider,
        new RoboLocationRequest(bestProvider, minTime, minDistance, false),
        executor,
        listener);
  }

  @Implementation
  protected void requestLocationUpdates(
      String provider, long minTime, float minDistance, PendingIntent pendingIntent) {
    requestLocationUpdatesInternal(
        provider, new RoboLocationRequest(provider, minTime, minDistance, false), pendingIntent);
  }

  @Implementation
  protected void requestLocationUpdates(
      long minTime, float minDistance, Criteria criteria, PendingIntent pendingIntent) {
    String bestProvider = getBestProvider(criteria, true);
    if (bestProvider == null) {
      throw new IllegalArgumentException("no providers found for criteria");
    }
    requestLocationUpdatesInternal(
        bestProvider,
        new RoboLocationRequest(bestProvider, minTime, minDistance, false),
        pendingIntent);
  }

  @Implementation(minSdk = VERSION_CODES.R)
  protected void requestLocationUpdates(
      @Nullable LocationRequest request, Executor executor, LocationListener listener) {
    if (request == null) {
      request = LocationRequest.create();
    }
    requestLocationUpdatesInternal(
        request.getProvider(), new RoboLocationRequest(request), executor, listener);
  }

  @Implementation(minSdk = VERSION_CODES.KITKAT)
  protected void requestLocationUpdates(
      @Nullable LocationRequest request, LocationListener listener, Looper looper) {
    if (request == null) {
      request = LocationRequest.create();
    }
    if (looper == null) {
      looper = Looper.myLooper();
      if (looper == null) {
        // forces appropriate exception
        new Handler();
      }
    }
    requestLocationUpdatesInternal(
        request.getProvider(),
        new RoboLocationRequest(request),
        new HandlerExecutor(new Handler(looper)),
        listener);
  }

  @Implementation(minSdk = VERSION_CODES.KITKAT)
  protected void requestLocationUpdates(
      @Nullable LocationRequest request, PendingIntent pendingIntent) {
    if (request == null) {
      request = LocationRequest.create();
    }
    requestLocationUpdatesInternal(
        request.getProvider(), new RoboLocationRequest(request), pendingIntent);
  }

  @Implementation(minSdk = VERSION_CODES.S)
  protected void requestLocationUpdates(
      String provider, LocationRequest request, Executor executor, LocationListener listener) {
    requestLocationUpdatesInternal(provider, new RoboLocationRequest(request), executor, listener);
  }

  @Implementation(minSdk = VERSION_CODES.S)
  protected void requestLocationUpdates(
      String provider, LocationRequest request, PendingIntent pendingIntent) {
    requestLocationUpdatesInternal(provider, new RoboLocationRequest(request), pendingIntent);
  }

  private void requestLocationUpdatesInternal(
      String provider, RoboLocationRequest request, Executor executor, LocationListener listener) {
    if (provider == null || request == null || executor == null || listener == null) {
      throw new IllegalArgumentException();
    }
    getOrCreateProviderEntry(provider).addListener(listener, request, executor);
  }

  private void requestLocationUpdatesInternal(
      String provider, RoboLocationRequest request, PendingIntent pendingIntent) {
    if (provider == null || request == null || pendingIntent == null) {
      throw new IllegalArgumentException();
    }
    getOrCreateProviderEntry(provider).addListener(pendingIntent, request);
  }

  @Implementation
  protected void removeUpdates(LocationListener listener) {
    removeUpdatesInternal(listener);
  }

  @Implementation
  protected void removeUpdates(PendingIntent pendingIntent) {
    removeUpdatesInternal(pendingIntent);
  }

  private void removeUpdatesInternal(Object key) {
    for (ProviderEntry providerEntry : getProviderEntries()) {
      providerEntry.removeListener(key);
    }
  }

  @Implementation(minSdk = VERSION_CODES.S)
  protected void requestFlush(String provider, LocationListener listener, int requestCode) {
    ProviderEntry entry = getProviderEntry(provider);
    if (entry == null) {
      throw new IllegalArgumentException("unknown provider \"" + provider + "\"");
    }

    entry.requestFlush(listener, requestCode);
  }

  @Implementation(minSdk = VERSION_CODES.S)
  protected void requestFlush(String provider, PendingIntent pendingIntent, int requestCode) {
    ProviderEntry entry = getProviderEntry(provider);
    if (entry == null) {
      throw new IllegalArgumentException("unknown provider \"" + provider + "\"");
    }

    entry.requestFlush(pendingIntent, requestCode);
  }

  /**
   * Returns the list of {@link LocationRequest} currently registered under the given provider.
   * Clients compiled against the public Android SDK should only use this method on S+, clients
   * compiled against the system Android SDK may only use this method on Kitkat+.
   *
   * 

Prior to Android S {@link LocationRequest} equality is not well defined, so prefer using * {@link #getLegacyLocationRequests(String)} instead if equality is required for testing. */ @RequiresApi(VERSION_CODES.KITKAT) public List getLocationRequests(String provider) { ProviderEntry providerEntry = getProviderEntry(provider); if (providerEntry == null) { return ImmutableList.of(); } return ImmutableList.copyOf( Iterables.transform( providerEntry.getTransports(), transport -> transport.getRequest().getLocationRequest())); } /** * Returns the list of {@link RoboLocationRequest} currently registered under the given provider. * Since {@link LocationRequest} was not publicly visible prior to S, and did not exist prior to * Kitkat, {@link RoboLocationRequest} allows querying the location requests prior to those * platforms, and also implements proper equality comparisons for testing. */ public List getLegacyLocationRequests(String provider) { ProviderEntry providerEntry = getProviderEntry(provider); if (providerEntry == null) { return ImmutableList.of(); } return ImmutableList.copyOf( Iterables.transform(providerEntry.getTransports(), LocationTransport::getRequest)); } @Implementation(minSdk = VERSION_CODES.P) protected boolean injectLocation(Location location) { return false; } @Implementation(minSdk = VERSION_CODES.O) protected int getGnssBatchSize() { return gnssBatchSize; } /** * Sets the GNSS hardware batch size. Values greater than 0 enables hardware GNSS batching APIs. */ public void setGnssBatchSize(int gnssBatchSize) { this.gnssBatchSize = gnssBatchSize; } @Implementation(minSdk = VERSION_CODES.O) protected boolean registerGnssBatchedLocationCallback( Object periodNanos, Object wakeOnFifoFull, Object callback, Object handler) { getOrCreateProviderEntry(GPS_PROVIDER) .setLegacyBatchedListener( callback, new HandlerExecutor((Handler) handler), gnssBatchSize, (Boolean) wakeOnFifoFull); return true; } @Implementation(minSdk = VERSION_CODES.O) protected void flushGnssBatch() { ProviderEntry e = getProviderEntry(GPS_PROVIDER); if (e != null) { e.flushLegacyBatch(); } } @Implementation(minSdk = VERSION_CODES.O) protected boolean unregisterGnssBatchedLocationCallback(Object callback) { ProviderEntry e = getProviderEntry(GPS_PROVIDER); if (e != null) { e.clearLegacyBatchedListener(); } return true; } @Implementation(minSdk = VERSION_CODES.P) @Nullable protected String getGnssHardwareModelName() { return gnssHardwareModelName; } /** * Sets the GNSS hardware model name returned by {@link * LocationManager#getGnssHardwareModelName()}. */ public void setGnssHardwareModelName(@Nullable String gnssHardwareModelName) { this.gnssHardwareModelName = gnssHardwareModelName; } @Implementation(minSdk = VERSION_CODES.P) protected int getGnssYearOfHardware() { return gnssYearOfHardware; } /** Sets the GNSS year of hardware returned by {@link LocationManager#getGnssYearOfHardware()}. */ public void setGnssYearOfHardware(int gnssYearOfHardware) { this.gnssYearOfHardware = gnssYearOfHardware; } @Implementation protected boolean addGpsStatusListener(GpsStatus.Listener listener) { if (RuntimeEnvironment.getApiLevel() > VERSION_CODES.R) { throw new UnsupportedOperationException( "GpsStatus APIs not supported, please use GnssStatus APIs instead"); } synchronized (gpsStatusListeners) { gpsStatusListeners.add(listener); } return true; } @Implementation protected void removeGpsStatusListener(GpsStatus.Listener listener) { if (RuntimeEnvironment.getApiLevel() > VERSION_CODES.R) { throw new UnsupportedOperationException( "GpsStatus APIs not supported, please use GnssStatus APIs instead"); } synchronized (gpsStatusListeners) { gpsStatusListeners.remove(listener); } } /** Returns the list of currently registered {@link GpsStatus.Listener}s. */ public List getGpsStatusListeners() { synchronized (gpsStatusListeners) { return new ArrayList<>(gpsStatusListeners); } } @Implementation(minSdk = VERSION_CODES.N) protected boolean registerGnssStatusCallback(GnssStatus.Callback callback, Handler handler) { if (handler == null) { handler = new Handler(); } return registerGnssStatusCallback(new HandlerExecutor(handler), callback); } @Implementation(minSdk = VERSION_CODES.R) protected boolean registerGnssStatusCallback(Executor executor, GnssStatus.Callback listener) { synchronized (gnssStatusTransports) { Iterables.removeIf(gnssStatusTransports, transport -> transport.getListener() == listener); gnssStatusTransports.add(new GnssStatusCallbackTransport(executor, listener)); } return true; } @Implementation(minSdk = VERSION_CODES.N) protected void unregisterGnssStatusCallback(GnssStatus.Callback listener) { synchronized (gnssStatusTransports) { Iterables.removeIf(gnssStatusTransports, transport -> transport.getListener() == listener); } } /** Simulates a GNSS status started event. */ @RequiresApi(VERSION_CODES.N) public void simulateGnssStatusStarted() { List transports; synchronized (gnssStatusTransports) { transports = gnssStatusTransports; } for (GnssStatusCallbackTransport transport : transports) { transport.onStarted(); } } /** Simulates a GNSS status first fix event. */ @RequiresApi(VERSION_CODES.N) public void simulateGnssStatusFirstFix(int ttff) { List transports; synchronized (gnssStatusTransports) { transports = gnssStatusTransports; } for (GnssStatusCallbackTransport transport : transports) { transport.onFirstFix(ttff); } } /** Simulates a GNSS status event. */ @RequiresApi(VERSION_CODES.N) public void simulateGnssStatus(GnssStatus status) { List transports; synchronized (gnssStatusTransports) { transports = gnssStatusTransports; } for (GnssStatusCallbackTransport transport : transports) { transport.onSatelliteStatusChanged(status); } } /** * @deprecated Use {@link #simulateGnssStatus(GnssStatus)} instead. */ @Deprecated @RequiresApi(VERSION_CODES.N) public void sendGnssStatus(GnssStatus status) { simulateGnssStatus(status); } /** Simulates a GNSS status stopped event. */ @RequiresApi(VERSION_CODES.N) public void simulateGnssStatusStopped() { List transports; synchronized (gnssStatusTransports) { transports = gnssStatusTransports; } for (GnssStatusCallbackTransport transport : transports) { transport.onStopped(); } } @Implementation(minSdk = VERSION_CODES.N) protected boolean addNmeaListener(OnNmeaMessageListener listener, Handler handler) { if (handler == null) { handler = new Handler(); } return addNmeaListener(new HandlerExecutor(handler), listener); } @Implementation(minSdk = VERSION_CODES.R) protected boolean addNmeaListener(Executor executor, OnNmeaMessageListener listener) { synchronized (nmeaMessageTransports) { Iterables.removeIf(nmeaMessageTransports, transport -> transport.getListener() == listener); nmeaMessageTransports.add(new OnNmeaMessageListenerTransport(executor, listener)); } return true; } @Implementation(minSdk = VERSION_CODES.N) protected void removeNmeaListener(OnNmeaMessageListener listener) { synchronized (nmeaMessageTransports) { Iterables.removeIf(nmeaMessageTransports, transport -> transport.getListener() == listener); } } /** Simulates a NMEA message. */ @RequiresApi(api = VERSION_CODES.N) public void simulateNmeaMessage(String message, long timestamp) { List transports; synchronized (nmeaMessageTransports) { transports = nmeaMessageTransports; } for (OnNmeaMessageListenerTransport transport : transports) { transport.onNmeaMessage(message, timestamp); } } /** * @deprecated Use {@link #simulateNmeaMessage(String, long)} instead. */ @Deprecated @RequiresApi(api = VERSION_CODES.N) public void sendNmeaMessage(String message, long timestamp) { simulateNmeaMessage(message, timestamp); } @Implementation(minSdk = VERSION_CODES.N) protected boolean registerGnssMeasurementsCallback( GnssMeasurementsEvent.Callback listener, Handler handler) { if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.R) { if (handler == null) { handler = new Handler(); } return registerGnssMeasurementsCallback(new HandlerExecutor(handler), listener); } else { return registerGnssMeasurementsCallback(Runnable::run, listener); } } @Implementation(minSdk = VERSION_CODES.R) @RequiresApi(api = VERSION_CODES.R) protected boolean registerGnssMeasurementsCallback( Object request, Object executor, Object callback) { return registerGnssMeasurementsCallback( (Executor) executor, (GnssMeasurementsEvent.Callback) callback); } @Implementation(minSdk = VERSION_CODES.R) protected boolean registerGnssMeasurementsCallback( Executor executor, GnssMeasurementsEvent.Callback listener) { synchronized (gnssMeasurementTransports) { Iterables.removeIf( gnssMeasurementTransports, transport -> transport.getListener() == listener); gnssMeasurementTransports.add(new GnssMeasurementsEventCallbackTransport(executor, listener)); } return true; } @Implementation(minSdk = VERSION_CODES.N) protected void unregisterGnssMeasurementsCallback(GnssMeasurementsEvent.Callback listener) { synchronized (gnssMeasurementTransports) { Iterables.removeIf( gnssMeasurementTransports, transport -> transport.getListener() == listener); } } /** Simulates a GNSS measurements event. */ @RequiresApi(api = VERSION_CODES.N) public void simulateGnssMeasurementsEvent(GnssMeasurementsEvent event) { List transports; synchronized (gnssMeasurementTransports) { transports = gnssMeasurementTransports; } for (GnssMeasurementsEventCallbackTransport transport : transports) { transport.onGnssMeasurementsReceived(event); } } /** * @deprecated Use {@link #simulateGnssMeasurementsEvent(GnssMeasurementsEvent)} instead. */ @Deprecated @RequiresApi(api = VERSION_CODES.N) public void sendGnssMeasurementsEvent(GnssMeasurementsEvent event) { simulateGnssMeasurementsEvent(event); } /** Simulates a GNSS measurements status change. */ @RequiresApi(api = VERSION_CODES.N) public void simulateGnssMeasurementsStatus(int status) { List transports; synchronized (gnssMeasurementTransports) { transports = gnssMeasurementTransports; } for (GnssMeasurementsEventCallbackTransport transport : transports) { transport.onStatusChanged(status); } } @Implementation(minSdk = VERSION_CODES.R) protected Object registerAntennaInfoListener(Object executor, Object listener) { synchronized (gnssAntennaInfoTransports) { Iterables.removeIf( gnssAntennaInfoTransports, transport -> transport.getListener() == listener); gnssAntennaInfoTransports.add( new GnssAntennaInfoListenerTransport( (Executor) executor, (GnssAntennaInfo.Listener) listener)); } return true; } @Implementation(minSdk = VERSION_CODES.R) protected void unregisterAntennaInfoListener(Object listener) { synchronized (gnssAntennaInfoTransports) { Iterables.removeIf( gnssAntennaInfoTransports, transport -> transport.getListener() == listener); } } /** Simulates a GNSS antenna info event. */ @RequiresApi(api = VERSION_CODES.R) public void simulateGnssAntennaInfo(List antennaInfos) { List transports; synchronized (gnssAntennaInfoTransports) { transports = gnssAntennaInfoTransports; } for (GnssAntennaInfoListenerTransport transport : transports) { transport.onGnssAntennaInfoReceived(new ArrayList<>(antennaInfos)); } } /** * @deprecated Use {@link #simulateGnssAntennaInfo(List)} instead. */ @Deprecated @RequiresApi(api = VERSION_CODES.R) public void sendGnssAntennaInfo(List antennaInfos) { simulateGnssAntennaInfo(antennaInfos); } /** * A convenience function equivalent to invoking {@link #simulateLocation(String, Location)} with * the provider of the given location. */ public void simulateLocation(Location location) { simulateLocation(location.getProvider(), location); } /** * Delivers to the given provider (which will be created if necessary) a new location which will * be delivered to appropriate listeners and updates state accordingly. Delivery will ignore the * enabled/disabled state of providers, unlike location on a real device. * *

The location will also be delivered to the passive provider. */ public void simulateLocation(String provider, Location... locations) { ProviderEntry providerEntry = getOrCreateProviderEntry(provider); if (!PASSIVE_PROVIDER.equals(providerEntry.getName())) { providerEntry.simulateLocation(locations); } ProviderEntry passiveProviderEntry = getProviderEntry(PASSIVE_PROVIDER); if (passiveProviderEntry != null) { passiveProviderEntry.simulateLocation(locations); } } /** * @deprecated Do not test listeners, instead use {@link #simulateLocation(Location)} and test the * results of those listeners being invoked. */ @Deprecated public List getRequestLocationUpdateListeners() { return getLocationUpdateListeners(); } /** * @deprecated Do not test listeners, instead use {@link #simulateLocation(Location)} and test the * results of those listeners being invoked. */ @Deprecated public List getLocationUpdateListeners() { HashSet listeners = new HashSet<>(); for (ProviderEntry providerEntry : getProviderEntries()) { Iterables.addAll( listeners, Iterables.transform( Iterables.filter(providerEntry.getTransports(), LocationListenerTransport.class), LocationTransport::getKey)); } return new ArrayList<>(listeners); } /** * @deprecated Do not test listeners, instead use {@link #simulateLocation(Location)} and test the * results of those listeners being invoked. */ @Deprecated public List getLocationUpdateListeners(String provider) { ProviderEntry providerEntry = getProviderEntry(provider); if (providerEntry == null) { return Collections.emptyList(); } HashSet listeners = new HashSet<>(); Iterables.addAll( listeners, Iterables.transform( Iterables.filter(providerEntry.getTransports(), LocationListenerTransport.class), LocationTransport::getKey)); return new ArrayList<>(listeners); } /** * @deprecated Do not test pending intents, instead use {@link #simulateLocation(Location)} and * test the results of those pending intent being invoked. */ @Deprecated public List getLocationUpdatePendingIntents() { HashSet listeners = new HashSet<>(); for (ProviderEntry providerEntry : getProviderEntries()) { Iterables.addAll( listeners, Iterables.transform( Iterables.filter(providerEntry.getTransports(), LocationPendingIntentTransport.class), LocationTransport::getKey)); } return new ArrayList<>(listeners); } /** * Retrieves a list of all currently registered pending intents for the given provider. * * @deprecated Do not test pending intents, instead use {@link #simulateLocation(Location)} and * test the results of those pending intent being invoked. */ @Deprecated public List getLocationUpdatePendingIntents(String provider) { ProviderEntry providerEntry = getProviderEntry(provider); if (providerEntry == null) { return Collections.emptyList(); } HashSet listeners = new HashSet<>(); Iterables.addAll( listeners, Iterables.transform( Iterables.filter(providerEntry.getTransports(), LocationPendingIntentTransport.class), LocationTransport::getKey)); return new ArrayList<>(listeners); } private Context getContext() { return ReflectionHelpers.getField(realLocationManager, "mContext"); } private ProviderEntry getOrCreateProviderEntry(String name) { if (name == null) { throw new IllegalArgumentException("cannot use a null provider"); } synchronized (providers) { ProviderEntry providerEntry = getProviderEntry(name); if (providerEntry == null) { providerEntry = new ProviderEntry(name, null); providers.add(providerEntry); } return providerEntry; } } @Nullable private ProviderEntry getProviderEntry(String name) { if (name == null) { return null; } synchronized (providers) { for (ProviderEntry providerEntry : providers) { if (name.equals(providerEntry.getName())) { return providerEntry; } } } return null; } private Set getProviderEntries() { synchronized (providers) { return providers; } } private void removeProviderEntry(String name) { synchronized (providers) { providers.remove(getProviderEntry(name)); } } // provider enabled logic is complicated due to many changes over different versions of android. a // brief explanation of how the logic works in this shadow (which is subtly different and more // complicated from how the logic works in real android): // // 1) prior to P, the source of truth for whether a provider is enabled must be the // LOCATION_PROVIDERS_ALLOWED setting, so that direct writes into that setting are respected. // changes to the network and gps providers must change LOCATION_MODE appropriately as well. // 2) for P, providers are considered enabled if the LOCATION_MODE setting is not off AND they are // enabled via LOCATION_PROVIDERS_ALLOWED. direct writes into LOCATION_PROVIDERS_ALLOWED should // be respected (if the LOCATION_MODE is not off). changes to LOCATION_MODE will change the // state of the network and gps providers. // 3) for Q/R, providers are considered enabled if the LOCATION_MODE settings is not off AND they // are enabled, but the store for the enabled state may not be LOCATION_PROVIDERS_ALLOWED, as // writes into LOCATION_PROVIDERS_ALLOWED should not be respected. LOCATION_PROVIDERS_ALLOWED // should still be updated so that provider state changes can be listened to via that setting. // changes to LOCATION_MODE should not change the state of the network and gps provider. // 5) the passive provider is always special-cased at all API levels - it's state is controlled // programmatically, and should never be determined by LOCATION_PROVIDERS_ALLOWED. private final class ProviderEntry { private final String name; @GuardedBy("this") private final CopyOnWriteArrayList> locationTransports = new CopyOnWriteArrayList<>(); @GuardedBy("this") @Nullable private LegacyBatchedTransport legacyBatchedTransport; @GuardedBy("this") @Nullable private ProviderProperties properties; @GuardedBy("this") private boolean enabled; @GuardedBy("this") @Nullable private Location lastLocation; ProviderEntry(String name, @Nullable ProviderProperties properties) { this.name = name; this.properties = properties; switch (name) { case PASSIVE_PROVIDER: // passive provider always starts enabled enabled = true; break; case GPS_PROVIDER: enabled = ShadowSecure.INITIAL_GPS_PROVIDER_STATE; break; case NETWORK_PROVIDER: enabled = ShadowSecure.INITIAL_NETWORK_PROVIDER_STATE; break; default: enabled = false; break; } } public String getName() { return name; } public synchronized List> getTransports() { return locationTransports; } @Nullable public synchronized ProviderProperties getProperties() { return properties; } public synchronized void setProperties(@Nullable ProviderProperties properties) { this.properties = properties; } public boolean isEnabled() { if (PASSIVE_PROVIDER.equals(name) || RuntimeEnvironment.getApiLevel() >= VERSION_CODES.Q) { synchronized (this) { return enabled; } } else { String allowedProviders = Secure.getString(getContext().getContentResolver(), LOCATION_PROVIDERS_ALLOWED); if (TextUtils.isEmpty(allowedProviders)) { return false; } else { return Arrays.asList(allowedProviders.split(",")).contains(name); } } } public void setEnabled(boolean enabled) { List> transports; synchronized (this) { if (PASSIVE_PROVIDER.equals(name)) { // the passive provider cannot be disabled, but the passive provider didn't exist in // previous versions of this shadow. for backwards compatibility, we let the passive // provider be disabled. this also help emulate the situation where an app only has COARSE // permissions, which this shadow normally can't emulate. this.enabled = enabled; return; } int oldLocationMode = getLocationMode(); int newLocationMode = oldLocationMode; if (RuntimeEnvironment.getApiLevel() < VERSION_CODES.P) { if (GPS_PROVIDER.equals(name)) { if (enabled) { switch (oldLocationMode) { case LOCATION_MODE_OFF: newLocationMode = LOCATION_MODE_SENSORS_ONLY; break; case LOCATION_MODE_BATTERY_SAVING: newLocationMode = LOCATION_MODE_HIGH_ACCURACY; break; default: break; } } else { switch (oldLocationMode) { case LOCATION_MODE_SENSORS_ONLY: newLocationMode = LOCATION_MODE_OFF; break; case LOCATION_MODE_HIGH_ACCURACY: newLocationMode = LOCATION_MODE_BATTERY_SAVING; break; default: break; } } } else if (NETWORK_PROVIDER.equals(name)) { if (enabled) { switch (oldLocationMode) { case LOCATION_MODE_OFF: newLocationMode = LOCATION_MODE_BATTERY_SAVING; break; case LOCATION_MODE_SENSORS_ONLY: newLocationMode = LOCATION_MODE_HIGH_ACCURACY; break; default: break; } } else { switch (oldLocationMode) { case LOCATION_MODE_BATTERY_SAVING: newLocationMode = LOCATION_MODE_OFF; break; case LOCATION_MODE_HIGH_ACCURACY: newLocationMode = LOCATION_MODE_SENSORS_ONLY; break; default: break; } } } } if (newLocationMode != oldLocationMode) { // this sets LOCATION_MODE and LOCATION_PROVIDERS_ALLOWED setLocationModeInternal(newLocationMode); } else if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.Q) { if (enabled == this.enabled) { return; } this.enabled = enabled; // set LOCATION_PROVIDERS_ALLOWED directly, without setting LOCATION_MODE. do this even // though LOCATION_PROVIDERS_ALLOWED is not the source of truth - we keep it up to date, // but ignore any direct writes to it ShadowSettings.ShadowSecure.updateEnabledProviders( getContext().getContentResolver(), name, enabled); } else { if (enabled == this.enabled) { return; } this.enabled = enabled; // set LOCATION_PROVIDERS_ALLOWED directly, without setting LOCATION_MODE ShadowSettings.ShadowSecure.updateEnabledProviders( getContext().getContentResolver(), name, enabled); } transports = locationTransports; } for (LocationTransport transport : transports) { if (!transport.invokeOnProviderEnabled(name, enabled)) { synchronized (this) { Iterables.removeIf(locationTransports, current -> current == transport); } } } } @Nullable public synchronized Location getLastLocation() { return lastLocation; } public synchronized void setLastLocation(@Nullable Location location) { lastLocation = location; } public void simulateLocation(Location... locations) { List> transports; LegacyBatchedTransport batchedTransport; synchronized (this) { lastLocation = new Location(locations[locations.length - 1]); transports = locationTransports; batchedTransport = legacyBatchedTransport; } if (batchedTransport != null) { batchedTransport.invokeOnLocations(locations); } for (LocationTransport transport : transports) { if (!transport.invokeOnLocations(locations)) { synchronized (this) { Iterables.removeIf(locationTransports, current -> current == transport); } } } } public synchronized boolean meetsCriteria(Criteria criteria) { if (PASSIVE_PROVIDER.equals(name)) { return false; } if (properties == null) { return false; } return properties.meetsCriteria(criteria); } public void addListener( LocationListener listener, RoboLocationRequest request, Executor executor) { addListenerInternal(new LocationListenerTransport(listener, request, executor)); } public void addListener(PendingIntent pendingIntent, RoboLocationRequest request) { addListenerInternal(new LocationPendingIntentTransport(getContext(), pendingIntent, request)); } public void setLegacyBatchedListener( Object callback, Executor executor, int batchSize, boolean flushOnFifoFull) { synchronized (this) { legacyBatchedTransport = new LegacyBatchedTransport(callback, executor, batchSize, flushOnFifoFull); } } public void flushLegacyBatch() { LegacyBatchedTransport batchedTransport; synchronized (this) { batchedTransport = legacyBatchedTransport; } if (batchedTransport != null) { batchedTransport.invokeFlush(); } } public void clearLegacyBatchedListener() { synchronized (this) { legacyBatchedTransport = null; } } private void addListenerInternal(LocationTransport transport) { boolean invokeOnProviderEnabled; synchronized (this) { Iterables.removeIf(locationTransports, current -> current.getKey() == transport.getKey()); locationTransports.add(transport); invokeOnProviderEnabled = !enabled; } if (invokeOnProviderEnabled) { if (!transport.invokeOnProviderEnabled(name, false)) { synchronized (this) { Iterables.removeIf(locationTransports, current -> current == transport); } } } } public synchronized void removeListener(Object key) { Iterables.removeIf(locationTransports, transport -> transport.getKey() == key); } public void requestFlush(Object key, int requestCode) { LocationTransport transport; synchronized (this) { transport = Iterables.tryFind(locationTransports, t -> t.getKey() == key).orNull(); } if (transport == null) { throw new IllegalArgumentException("unregistered listener cannot be flushed"); } if (!transport.invokeOnFlush(requestCode)) { synchronized (this) { Iterables.removeIf(locationTransports, current -> current == transport); } } } @Override public boolean equals(Object o) { if (o instanceof ProviderEntry) { ProviderEntry that = (ProviderEntry) o; return Objects.equals(name, that.name); } return false; } @Override public int hashCode() { return Objects.hashCode(name); } } /** * LocationRequest doesn't exist prior to Kitkat, and is not public prior to S, so a new class is * required to represent it prior to those platforms. */ public static final class RoboLocationRequest { @Nullable private final Object locationRequest; // all these parameters are meaningless if locationRequest is set private final long intervalMillis; private final float minUpdateDistanceMeters; private final boolean singleShot; @RequiresApi(VERSION_CODES.KITKAT) public RoboLocationRequest(LocationRequest locationRequest) { this.locationRequest = Objects.requireNonNull(locationRequest); intervalMillis = 0; minUpdateDistanceMeters = 0; singleShot = false; } public RoboLocationRequest( String provider, long intervalMillis, float minUpdateDistanceMeters, boolean singleShot) { if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.KITKAT) { locationRequest = LocationRequest.createFromDeprecatedProvider( provider, intervalMillis, minUpdateDistanceMeters, singleShot); } else { locationRequest = null; } this.intervalMillis = intervalMillis; this.minUpdateDistanceMeters = minUpdateDistanceMeters; this.singleShot = singleShot; } @RequiresApi(VERSION_CODES.KITKAT) public LocationRequest getLocationRequest() { return (LocationRequest) Objects.requireNonNull(locationRequest); } public long getIntervalMillis() { if (locationRequest != null) { return ((LocationRequest) locationRequest).getInterval(); } else { return intervalMillis; } } public float getMinUpdateDistanceMeters() { if (locationRequest != null) { return ((LocationRequest) locationRequest).getSmallestDisplacement(); } else { return minUpdateDistanceMeters; } } public boolean isSingleShot() { if (locationRequest != null) { return ((LocationRequest) locationRequest).getNumUpdates() == 1; } else { return singleShot; } } long getMinUpdateIntervalMillis() { if (locationRequest != null) { return ((LocationRequest) locationRequest).getFastestInterval(); } else { return intervalMillis; } } int getMaxUpdates() { if (locationRequest != null) { return ((LocationRequest) locationRequest).getNumUpdates(); } else { return singleShot ? 1 : Integer.MAX_VALUE; } } @Override public boolean equals(Object o) { if (o instanceof RoboLocationRequest) { RoboLocationRequest that = (RoboLocationRequest) o; // location request equality is not well-defined prior to S if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.S) { return Objects.equals(locationRequest, that.locationRequest); } else { if (intervalMillis != that.intervalMillis || singleShot != that.singleShot || Float.compare(that.minUpdateDistanceMeters, minUpdateDistanceMeters) != 0 || (locationRequest == null) != (that.locationRequest == null)) { return false; } if (locationRequest != null) { LocationRequest lr = (LocationRequest) locationRequest; LocationRequest thatLr = (LocationRequest) that.locationRequest; if (lr.getQuality() != thatLr.getQuality() || lr.getInterval() != thatLr.getInterval() || lr.getFastestInterval() != thatLr.getFastestInterval() || lr.getExpireAt() != thatLr.getExpireAt() || lr.getNumUpdates() != thatLr.getNumUpdates() || lr.getSmallestDisplacement() != thatLr.getSmallestDisplacement() || lr.getHideFromAppOps() != thatLr.getHideFromAppOps() || !Objects.equals(lr.getProvider(), thatLr.getProvider())) { return false; } // allow null worksource to match empty worksource WorkSource workSource = lr.getWorkSource() == null ? new WorkSource() : lr.getWorkSource(); WorkSource thatWorkSource = thatLr.getWorkSource() == null ? new WorkSource() : thatLr.getWorkSource(); if (!workSource.equals(thatWorkSource)) { return false; } if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.Q) { if (lr.isLowPowerMode() != thatLr.isLowPowerMode() || lr.isLocationSettingsIgnored() != thatLr.isLocationSettingsIgnored()) { return false; } } } return true; } } return false; } @Override public int hashCode() { if (locationRequest != null) { return locationRequest.hashCode(); } else { return Objects.hash(intervalMillis, singleShot, minUpdateDistanceMeters); } } @Override public String toString() { if (locationRequest != null) { return locationRequest.toString(); } else { return "Request[interval=" + intervalMillis + ", minUpdateDistance=" + minUpdateDistanceMeters + ", singleShot=" + singleShot + "]"; } } } private abstract static class LocationTransport { private final KeyT key; private final RoboLocationRequest request; private Location lastDeliveredLocation; private int numDeliveries; LocationTransport(KeyT key, RoboLocationRequest request) { if (key == null) { throw new IllegalArgumentException(); } this.key = key; this.request = request; } public KeyT getKey() { return key; } public RoboLocationRequest getRequest() { return request; } // return false if this listener should be removed by this invocation public boolean invokeOnLocations(Location... locations) { ArrayList deliverableLocations = new ArrayList<>(locations.length); for (Location location : locations) { if (lastDeliveredLocation != null) { if (location.getTime() - lastDeliveredLocation.getTime() < request.getMinUpdateIntervalMillis()) { Log.w(TAG, "location rejected for simulated delivery - too fast"); continue; } if (distanceBetween(location, lastDeliveredLocation) < request.getMinUpdateDistanceMeters()) { Log.w(TAG, "location rejected for simulated delivery - too close"); continue; } } deliverableLocations.add(new Location(location)); lastDeliveredLocation = new Location(location); } if (deliverableLocations.isEmpty()) { return true; } boolean needsRemoval = false; numDeliveries += deliverableLocations.size(); if (numDeliveries >= request.getMaxUpdates()) { needsRemoval = true; } try { if (deliverableLocations.size() == 1) { onLocation(deliverableLocations.get(0)); } else { onLocations(deliverableLocations); } } catch (CanceledException e) { needsRemoval = true; } return !needsRemoval; } // return false if this listener should be removed by this invocation public boolean invokeOnProviderEnabled(String provider, boolean enabled) { try { onProviderEnabled(provider, enabled); return true; } catch (CanceledException e) { return false; } } // return false if this listener should be removed by this invocation public boolean invokeOnFlush(int requestCode) { try { onFlushComplete(requestCode); return true; } catch (CanceledException e) { return false; } } abstract void onLocation(Location location) throws CanceledException; abstract void onLocations(List locations) throws CanceledException; abstract void onProviderEnabled(String provider, boolean enabled) throws CanceledException; abstract void onFlushComplete(int requestCode) throws CanceledException; } private static final class LocationListenerTransport extends LocationTransport { private final Executor executor; LocationListenerTransport( LocationListener key, RoboLocationRequest request, Executor executor) { super(key, request); this.executor = executor; } @Override void onLocation(Location location) { executor.execute(() -> getKey().onLocationChanged(location)); } @Override void onLocations(List locations) { executor.execute( () -> { if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.S) { getKey().onLocationChanged(locations); } else { for (Location location : locations) { getKey().onLocationChanged(location); } } }); } @Override void onProviderEnabled(String provider, boolean enabled) { executor.execute( () -> { if (enabled) { getKey().onProviderEnabled(provider); } else { getKey().onProviderDisabled(provider); } }); } @Override void onFlushComplete(int requestCode) { executor.execute(() -> getKey().onFlushComplete(requestCode)); } } private static final class LocationPendingIntentTransport extends LocationTransport { private final Context context; LocationPendingIntentTransport( Context context, PendingIntent key, RoboLocationRequest request) { super(key, request); this.context = context; } @Override void onLocation(Location location) throws CanceledException { Intent intent = new Intent(); intent.putExtra(LocationManager.KEY_LOCATION_CHANGED, new Location(location)); getKey().send(context, 0, intent); } @Override void onLocations(List locations) throws CanceledException { if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.S) { Intent intent = new Intent(); intent.putExtra(LocationManager.KEY_LOCATION_CHANGED, locations.get(locations.size() - 1)); intent.putExtra(LocationManager.KEY_LOCATIONS, locations.toArray(new Location[0])); getKey().send(context, 0, intent); } else { for (Location location : locations) { onLocation(location); } } } @Override void onProviderEnabled(String provider, boolean enabled) throws CanceledException { Intent intent = new Intent(); intent.putExtra(LocationManager.KEY_PROVIDER_ENABLED, enabled); getKey().send(context, 0, intent); } @Override void onFlushComplete(int requestCode) throws CanceledException { Intent intent = new Intent(); intent.putExtra(LocationManager.KEY_FLUSH_COMPLETE, requestCode); getKey().send(context, 0, intent); } } private static final class LegacyBatchedTransport { private final android.location.BatchedLocationCallback callback; private final Executor executor; private final int batchSize; private final boolean flushOnFifoFull; private ArrayList batch = new ArrayList<>(); LegacyBatchedTransport( Object callback, Executor executor, int batchSize, boolean flushOnFifoFull) { this.callback = (android.location.BatchedLocationCallback) callback; this.executor = executor; this.batchSize = batchSize; this.flushOnFifoFull = flushOnFifoFull; } public void invokeFlush() { ArrayList delivery = batch; batch = new ArrayList<>(); executor.execute( () -> { callback.onLocationBatch(delivery); if (!delivery.isEmpty()) { callback.onLocationBatch(new ArrayList<>()); } }); } public void invokeOnLocations(Location... locations) { for (Location location : locations) { batch.add(new Location(location)); if (batch.size() >= batchSize) { if (!flushOnFifoFull) { batch.remove(0); } else { ArrayList delivery = batch; batch = new ArrayList<>(); executor.execute(() -> callback.onLocationBatch(delivery)); } } } } } /** * Returns the distance between the two locations in meters. Adapted from: * http://stackoverflow.com/questions/837872/calculate-distance-in-meters-when-you-know-longitude-and-latitude-in-java */ static float distanceBetween(Location location1, Location location2) { double earthRadius = 3958.75; double latDifference = Math.toRadians(location2.getLatitude() - location1.getLatitude()); double lonDifference = Math.toRadians(location2.getLongitude() - location1.getLongitude()); double a = Math.sin(latDifference / 2) * Math.sin(latDifference / 2) + Math.cos(Math.toRadians(location1.getLatitude())) * Math.cos(Math.toRadians(location2.getLatitude())) * Math.sin(lonDifference / 2) * Math.sin(lonDifference / 2); double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); double dist = Math.abs(earthRadius * c); int meterConversion = 1609; return (float) (dist * meterConversion); } @Resetter public static synchronized void reset() { locationProviderConstructor = null; } @RequiresApi(api = VERSION_CODES.N) private final class CurrentLocationTransport implements LocationListener { private final Executor executor; private final Consumer consumer; private final Handler timeoutHandler; @GuardedBy("this") private boolean triggered; @Nullable Runnable timeoutRunnable; CurrentLocationTransport(Executor executor, Consumer consumer) { this.executor = executor; this.consumer = consumer; timeoutHandler = new Handler(Looper.getMainLooper()); } public void cancel() { synchronized (this) { if (triggered) { return; } triggered = true; } cleanup(); } public void startTimeout(long timeoutMs) { synchronized (this) { if (triggered) { return; } timeoutRunnable = () -> { timeoutRunnable = null; onLocationChanged((Location) null); }; timeoutHandler.postDelayed(timeoutRunnable, timeoutMs); } } @Override public void onStatusChanged(String provider, int status, Bundle extras) {} @Override public void onProviderEnabled(String provider) {} @Override public void onProviderDisabled(String provider) { onLocationChanged((Location) null); } @Override public void onLocationChanged(@Nullable Location location) { synchronized (this) { if (triggered) { return; } triggered = true; } executor.execute(() -> consumer.accept(location)); cleanup(); } private void cleanup() { removeUpdates(this); if (timeoutRunnable != null) { timeoutHandler.removeCallbacks(timeoutRunnable); timeoutRunnable = null; } } } private static final class GnssStatusCallbackTransport { private final Executor executor; private final GnssStatus.Callback listener; GnssStatusCallbackTransport(Executor executor, GnssStatus.Callback listener) { this.executor = Objects.requireNonNull(executor); this.listener = Objects.requireNonNull(listener); } GnssStatus.Callback getListener() { return listener; } @RequiresApi(api = VERSION_CODES.N) public void onStarted() { executor.execute(listener::onStarted); } @RequiresApi(api = VERSION_CODES.N) public void onFirstFix(int ttff) { executor.execute(() -> listener.onFirstFix(ttff)); } @RequiresApi(api = VERSION_CODES.N) public void onSatelliteStatusChanged(GnssStatus status) { executor.execute(() -> listener.onSatelliteStatusChanged(status)); } @RequiresApi(api = VERSION_CODES.N) public void onStopped() { executor.execute(listener::onStopped); } } private static final class OnNmeaMessageListenerTransport { private final Executor executor; private final OnNmeaMessageListener listener; OnNmeaMessageListenerTransport(Executor executor, OnNmeaMessageListener listener) { this.executor = Objects.requireNonNull(executor); this.listener = Objects.requireNonNull(listener); } OnNmeaMessageListener getListener() { return listener; } @RequiresApi(api = VERSION_CODES.N) public void onNmeaMessage(String message, long timestamp) { executor.execute(() -> listener.onNmeaMessage(message, timestamp)); } } private static final class GnssMeasurementsEventCallbackTransport { private final Executor executor; private final GnssMeasurementsEvent.Callback listener; GnssMeasurementsEventCallbackTransport( Executor executor, GnssMeasurementsEvent.Callback listener) { this.executor = Objects.requireNonNull(executor); this.listener = Objects.requireNonNull(listener); } GnssMeasurementsEvent.Callback getListener() { return listener; } @RequiresApi(api = VERSION_CODES.N) public void onStatusChanged(int status) { executor.execute(() -> listener.onStatusChanged(status)); } @RequiresApi(api = VERSION_CODES.N) public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) { executor.execute(() -> listener.onGnssMeasurementsReceived(event)); } } private static final class GnssAntennaInfoListenerTransport { private final Executor executor; private final GnssAntennaInfo.Listener listener; GnssAntennaInfoListenerTransport(Executor executor, GnssAntennaInfo.Listener listener) { this.executor = Objects.requireNonNull(executor); this.listener = Objects.requireNonNull(listener); } GnssAntennaInfo.Listener getListener() { return listener; } @RequiresApi(api = VERSION_CODES.R) public void onGnssAntennaInfoReceived(List antennaInfos) { executor.execute(() -> listener.onGnssAntennaInfoReceived(antennaInfos)); } } private static final class HandlerExecutor implements Executor { private final Handler handler; HandlerExecutor(Handler handler) { this.handler = Objects.requireNonNull(handler); } @Override public void execute(Runnable command) { if (!handler.post(command)) { throw new RejectedExecutionException(handler + " is shutting down"); } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy