blob: 58ce522cf93f916017d7caecd1a3114269804125 [file] [log] [blame]
package org.robolectric.shadows;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Intent;
import android.location.Criteria;
import android.location.GpsStatus.Listener;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Looper;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@Implements(LocationManager.class)
public class ShadowLocationManager {
private final Map<String, LocationProviderEntry> providersEnabled = new LinkedHashMap<>();
private final Map<String, Location> lastKnownLocations = new HashMap<>();
private final Map<PendingIntent, Criteria> requestLocationUdpateCriteriaPendingIntents = new HashMap<>();
private final Map<PendingIntent, String> requestLocationUdpateProviderPendingIntents = new HashMap<>();
private final ArrayList<LocationListener> removedLocationListeners = new ArrayList<>();
private final ArrayList<Listener> gpsStatusListeners = new ArrayList<>();
private Criteria lastBestProviderCriteria;
private boolean lastBestProviderEnabled;
private String bestEnabledProvider, bestDisabledProvider;
/** Location listeners along with metadata on when they should be fired. */
private static final class ListenerRegistration {
final long minTime;
final float minDistance;
final LocationListener listener;
final String provider;
Location lastSeenLocation;
long lastSeenTime;
ListenerRegistration(String provider, long minTime, float minDistance, Location locationAtCreation,
LocationListener listener) {
this.provider = provider;
this.minTime = minTime;
this.minDistance = minDistance;
this.lastSeenTime = locationAtCreation == null ? 0 : locationAtCreation.getTime();
this.lastSeenLocation = locationAtCreation;
this.listener = listener;
}
}
/** Mapped by provider. */
private final Map<String, List<ListenerRegistration>> locationListeners =
new HashMap<>();
@Implementation
public boolean isProviderEnabled(String provider) {
LocationProviderEntry map = providersEnabled.get(provider);
if (map != null) {
Boolean isEnabled = map.getKey();
return isEnabled == null ? true : isEnabled;
}
return false;
}
@Implementation
public List<String> getAllProviders() {
Set<String> allKnownProviders = new LinkedHashSet<>(providersEnabled.keySet());
allKnownProviders.add(LocationManager.GPS_PROVIDER);
allKnownProviders.add(LocationManager.NETWORK_PROVIDER);
allKnownProviders.add(LocationManager.PASSIVE_PROVIDER);
return new ArrayList<>(allKnownProviders);
}
/**
* Sets the value to return from {@link #isProviderEnabled(String)} for the given {@code provider}
*
* @param provider
* name of the provider whose status to set
* @param isEnabled
* whether that provider should appear enabled
*/
public void setProviderEnabled(String provider, boolean isEnabled) {
setProviderEnabled(provider, isEnabled, null);
}
public void setProviderEnabled(String provider, boolean isEnabled, List<Criteria> criteria) {
LocationProviderEntry providerEntry = providersEnabled.get(provider);
if (providerEntry == null) {
providerEntry = new LocationProviderEntry();
}
providerEntry.enabled = isEnabled;
providerEntry.criteria = criteria;
providersEnabled.put(provider, providerEntry);
List<LocationListener> locationUpdateListeners = new ArrayList<>(getRequestLocationUpdateListeners());
for (LocationListener locationUpdateListener : locationUpdateListeners) {
if (isEnabled) {
locationUpdateListener.onProviderEnabled(provider);
} else {
locationUpdateListener.onProviderDisabled(provider);
}
}
// Send intent to notify about provider status
final Intent intent = new Intent();
intent.putExtra(LocationManager.KEY_PROVIDER_ENABLED, isEnabled);
ShadowApplication.getInstance().sendBroadcast(intent);
Set<PendingIntent> requestLocationUdpatePendingIntentSet = requestLocationUdpateCriteriaPendingIntents
.keySet();
for (PendingIntent requestLocationUdpatePendingIntent : requestLocationUdpatePendingIntentSet) {
try {
requestLocationUdpatePendingIntent.send();
} catch (CanceledException e) {
requestLocationUdpateCriteriaPendingIntents
.remove(requestLocationUdpatePendingIntent);
}
}
// if this provider gets disabled and it was the best active provider, then it's not anymore
if (provider.equals(bestEnabledProvider) && !isEnabled) {
bestEnabledProvider = null;
}
}
@Implementation
public List<String> getProviders(boolean enabledOnly) {
ArrayList<String> enabledProviders = new ArrayList<>();
for (String provider : getAllProviders()) {
if (!enabledOnly || providersEnabled.get(provider) != null) {
enabledProviders.add(provider);
}
}
return enabledProviders;
}
@Implementation
public Location getLastKnownLocation(String provider) {
return lastKnownLocations.get(provider);
}
@Implementation
public boolean addGpsStatusListener(Listener listener) {
if (!gpsStatusListeners.contains(listener)) {
gpsStatusListeners.add(listener);
}
return true;
}
@Implementation
public void removeGpsStatusListener(Listener listener) {
gpsStatusListeners.remove(listener);
}
@Implementation
public String getBestProvider(Criteria criteria, boolean enabled) {
lastBestProviderCriteria = criteria;
lastBestProviderEnabled = enabled;
if (criteria == null) {
return getBestProviderWithNoCriteria(enabled);
}
return getBestProviderWithCriteria(criteria, enabled);
}
private String getBestProviderWithCriteria(Criteria criteria, boolean enabled) {
List<String> providers = getProviders(enabled);
int powerRequirement = criteria.getPowerRequirement();
int accuracy = criteria.getAccuracy();
for (String provider : providers) {
LocationProviderEntry locationProviderEntry = providersEnabled.get(provider);
if (locationProviderEntry == null) {
continue;
}
List<Criteria> criteriaList = locationProviderEntry.getValue();
if (criteriaList == null) {
continue;
}
for (Criteria criteriaListItem : criteriaList) {
if (criteria.equals(criteriaListItem)) {
return provider;
} else if (criteriaListItem.getAccuracy() == accuracy) {
return provider;
} else if (criteriaListItem.getPowerRequirement() == powerRequirement) {
return provider;
}
}
}
// TODO: these conditions are incomplete
for (String provider : providers) {
if (provider.equals(LocationManager.NETWORK_PROVIDER) && (accuracy == Criteria.ACCURACY_COARSE || powerRequirement == Criteria.POWER_LOW)) {
return provider;
} else if (provider.equals(LocationManager.GPS_PROVIDER) && accuracy == Criteria.ACCURACY_FINE && powerRequirement != Criteria.POWER_LOW) {
return provider;
}
}
// No enabled provider found with the desired criteria, then return the the first registered provider(?)
return providers.isEmpty()? null : providers.get(0);
}
private String getBestProviderWithNoCriteria(boolean enabled) {
List<String> providers = getProviders(enabled);
if (enabled && bestEnabledProvider != null) {
return bestEnabledProvider;
} else if (bestDisabledProvider != null) {
return bestDisabledProvider;
} else if (providers.contains(LocationManager.GPS_PROVIDER)) {
return LocationManager.GPS_PROVIDER;
} else if (providers.contains(LocationManager.NETWORK_PROVIDER)) {
return LocationManager.NETWORK_PROVIDER;
}
return null;
}
@Implementation
public void requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener) {
addLocationListener(provider, listener, minTime, minDistance);
}
private void addLocationListener(String provider, LocationListener listener, long minTime, float minDistance) {
List<ListenerRegistration> providerListeners = locationListeners.get(provider);
if (providerListeners == null) {
providerListeners = new ArrayList<>();
locationListeners.put(provider, providerListeners);
}
removeDuplicates(listener, providerListeners);
providerListeners.add(new ListenerRegistration(provider,
minTime, minDistance, copyOf(getLastKnownLocation(provider)), listener));
}
private void removeDuplicates(LocationListener listener,
List<ListenerRegistration> providerListeners) {
final Iterator<ListenerRegistration> iterator = providerListeners.iterator();
while (iterator.hasNext()) {
if (iterator.next().listener.equals(listener)) {
iterator.remove();
}
}
}
@Implementation
public void requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener,
Looper looper) {
addLocationListener(provider, listener, minTime, minDistance);
}
@Implementation
public void requestLocationUpdates(long minTime, float minDistance, Criteria criteria, PendingIntent pendingIntent) {
if (pendingIntent == null) {
throw new IllegalStateException("Intent must not be null");
}
if (getBestProvider(criteria, true) == null) {
throw new IllegalArgumentException("no providers found for criteria");
}
requestLocationUdpateCriteriaPendingIntents.put(pendingIntent, criteria);
}
@Implementation
public void requestLocationUpdates(String provider, long minTime, float minDistance,
PendingIntent pendingIntent) {
if (pendingIntent == null) {
throw new IllegalStateException("Intent must not be null");
}
if (!providersEnabled.containsKey(provider)) {
throw new IllegalArgumentException("no providers found");
}
requestLocationUdpateProviderPendingIntents.put(pendingIntent, provider);
}
@Implementation
public void removeUpdates(LocationListener listener) {
removedLocationListeners.add(listener);
}
private void cleanupRemovedLocationListeners() {
for (Map.Entry<String, List<ListenerRegistration>> entry : locationListeners.entrySet()) {
List<ListenerRegistration> listenerRegistrations = entry.getValue();
for (int i = listenerRegistrations.size() - 1; i >= 0; i--) {
LocationListener listener = listenerRegistrations.get(i).listener;
if(removedLocationListeners.contains(listener)) {
listenerRegistrations.remove(i);
}
}
}
}
@Implementation
public void removeUpdates(PendingIntent pendingIntent) {
while (requestLocationUdpateCriteriaPendingIntents.remove(pendingIntent) != null);
while (requestLocationUdpateProviderPendingIntents.remove(pendingIntent) != null);
}
public boolean hasGpsStatusListener(Listener listener) {
return gpsStatusListeners.contains(listener);
}
/**
* Gets the criteria value used in the last call to {@link #getBestProvider(android.location.Criteria, boolean)}.
*
* @return the criteria used to find the best provider
*/
public Criteria getLastBestProviderCriteria() {
return lastBestProviderCriteria;
}
/**
* Gets the enabled value used in the last call to {@link #getBestProvider(android.location.Criteria, boolean)}
*
* @return the enabled value used to find the best provider
*/
public boolean getLastBestProviderEnabledOnly() {
return lastBestProviderEnabled;
}
/**
* Sets the value to return from {@link #getBestProvider(android.location.Criteria, boolean)} for the given
* {@code provider}
*
* @param provider name of the provider who should be considered best
* @param enabled Enabled
* @param criteria List of criteria
* @throws Exception if provider is not known
* @return false If provider is not enabled but it is supposed to be set as the best enabled provider don't set it, otherwise true
*/
public boolean setBestProvider(String provider, boolean enabled, List<Criteria> criteria) throws Exception {
if (!getAllProviders().contains(provider)) {
throw new IllegalStateException("Best provider is not a known provider");
}
// If provider is not enabled but it is supposed to be set as the best enabled provider don't set it.
for (String prvdr : providersEnabled.keySet()) {
if (provider.equals(prvdr) && providersEnabled.get(prvdr).enabled != enabled) {
return false;
}
}
if (enabled) {
bestEnabledProvider = provider;
if (provider.equals(bestDisabledProvider)) {
bestDisabledProvider = null;
}
} else {
bestDisabledProvider = provider;
if (provider.equals(bestEnabledProvider)) {
bestEnabledProvider = null;
}
}
if (criteria == null) {
return true;
}
LocationProviderEntry entry;
if (!providersEnabled.containsKey(provider)) {
entry = new LocationProviderEntry();
entry.enabled = enabled;
entry.criteria = criteria;
} else {
entry = providersEnabled.get(provider);
}
providersEnabled.put(provider, entry);
return true;
}
public boolean setBestProvider(String provider, boolean enabled) throws Exception {
return setBestProvider(provider, enabled, null);
}
/**
* Sets the value to return from {@link #getLastKnownLocation(String)} for the given {@code provider}
*
* @param provider
* name of the provider whose location to set
* @param location
* the last known location for the provider
*/
public void setLastKnownLocation(String provider, Location location) {
lastKnownLocations.put(provider, location);
}
/**
* @return lastRequestedLocationUpdatesLocationListener
*/
public List<LocationListener> getRequestLocationUpdateListeners() {
cleanupRemovedLocationListeners();
List<LocationListener> all = new ArrayList<>();
for (Map.Entry<String, List<ListenerRegistration>> entry : locationListeners.entrySet()) {
for (ListenerRegistration reg : entry.getValue()) {
all.add(reg.listener);
}
}
return all;
}
public void simulateLocation(Location location) {
cleanupRemovedLocationListeners();
setLastKnownLocation(location.getProvider(), location);
List<ListenerRegistration> providerListeners = locationListeners.get(
location.getProvider());
if (providerListeners == null) return;
for (ListenerRegistration listenerReg : providerListeners) {
if(listenerReg.lastSeenLocation != null && location != null) {
float distanceChange = distanceBetween(location, listenerReg.lastSeenLocation);
boolean withinMinDistance = distanceChange < listenerReg.minDistance;
boolean exceededMinTime = location.getTime() - listenerReg.lastSeenTime > listenerReg.minTime;
if (withinMinDistance || !exceededMinTime) continue;
}
listenerReg.lastSeenLocation = copyOf(location);
listenerReg.lastSeenTime = location == null ? 0 : location.getTime();
listenerReg.listener.onLocationChanged(copyOf(location));
}
cleanupRemovedLocationListeners();
}
private Location copyOf(Location location) {
if (location == null) return null;
Location copy = new Location(location);
copy.setAccuracy(location.getAccuracy());
copy.setAltitude(location.getAltitude());
copy.setBearing(location.getBearing());
copy.setExtras(location.getExtras());
copy.setLatitude(location.getLatitude());
copy.setLongitude(location.getLongitude());
copy.setProvider(location.getProvider());
copy.setSpeed(location.getSpeed());
copy.setTime(location.getTime());
return copy;
}
/**
* 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
*/
private 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);
}
public Map<PendingIntent, Criteria> getRequestLocationUdpateCriteriaPendingIntents() {
return requestLocationUdpateCriteriaPendingIntents;
}
public Map<PendingIntent, String> getRequestLocationUdpateProviderPendingIntents() {
return requestLocationUdpateProviderPendingIntents;
}
public Collection<String> getProvidersForListener(LocationListener listener) {
cleanupRemovedLocationListeners();
Set<String> providers = new HashSet<>();
for (List<ListenerRegistration> listenerRegistrations : locationListeners.values()) {
for (ListenerRegistration listenerRegistration : listenerRegistrations) {
if (listenerRegistration.listener == listener) {
providers.add(listenerRegistration.provider);
}
}
}
return providers;
}
final private static class LocationProviderEntry implements Map.Entry<Boolean, List<Criteria>> {
private Boolean enabled;
private List<Criteria> criteria;
@Override
public Boolean getKey() {
return enabled;
}
@Override
public List<Criteria> getValue() {
return criteria;
}
@Override
public List<Criteria> setValue(List<Criteria> criteria) {
List<Criteria> oldCriteria = this.criteria;
this.criteria = criteria;
return oldCriteria;
}
}
}