Home | History | Annotate | Download | only in shadows
      1 package org.robolectric.shadows;
      2 
      3 import static android.os.Build.VERSION_CODES.P;
      4 
      5 import android.app.PendingIntent;
      6 import android.app.PendingIntent.CanceledException;
      7 import android.content.Context;
      8 import android.content.Intent;
      9 import android.location.Criteria;
     10 import android.location.GpsStatus.Listener;
     11 import android.location.Location;
     12 import android.location.LocationListener;
     13 import android.location.LocationManager;
     14 import android.os.Looper;
     15 import android.os.UserHandle;
     16 import java.util.ArrayList;
     17 import java.util.Collection;
     18 import java.util.HashMap;
     19 import java.util.HashSet;
     20 import java.util.Iterator;
     21 import java.util.LinkedHashMap;
     22 import java.util.LinkedHashSet;
     23 import java.util.List;
     24 import java.util.Map;
     25 import java.util.Set;
     26 import org.robolectric.annotation.Implementation;
     27 import org.robolectric.annotation.Implements;
     28 import org.robolectric.annotation.RealObject;
     29 import org.robolectric.util.ReflectionHelpers;
     30 
     31 @Implements(LocationManager.class)
     32 public class ShadowLocationManager {
     33   @RealObject private LocationManager realLocationManager;
     34 
     35   private final Map<UserHandle, Boolean> locationEnabledForUser = new HashMap<>();
     36 
     37   private final Map<String, LocationProviderEntry> providersEnabled = new LinkedHashMap<>();
     38   private final Map<String, Location> lastKnownLocations = new HashMap<>();
     39   private final Map<PendingIntent, Criteria> requestLocationUdpateCriteriaPendingIntents = new HashMap<>();
     40   private final Map<PendingIntent, String> requestLocationUdpateProviderPendingIntents = new HashMap<>();
     41   private final ArrayList<LocationListener> removedLocationListeners = new ArrayList<>();
     42 
     43   private final ArrayList<Listener> gpsStatusListeners = new ArrayList<>();
     44   private Criteria lastBestProviderCriteria;
     45   private boolean lastBestProviderEnabled;
     46   private String bestEnabledProvider, bestDisabledProvider;
     47 
     48   /** Location listeners along with metadata on when they should be fired. */
     49   private static final class ListenerRegistration {
     50     final long minTime;
     51     final float minDistance;
     52     final LocationListener listener;
     53     final String provider;
     54     Location lastSeenLocation;
     55     long lastSeenTime;
     56 
     57     ListenerRegistration(String provider, long minTime, float minDistance, Location locationAtCreation,
     58                LocationListener listener) {
     59       this.provider = provider;
     60       this.minTime = minTime;
     61       this.minDistance = minDistance;
     62       this.lastSeenTime = locationAtCreation == null ? 0 : locationAtCreation.getTime();
     63       this.lastSeenLocation = locationAtCreation;
     64       this.listener = listener;
     65     }
     66   }
     67 
     68   /** Mapped by provider. */
     69   private final Map<String, List<ListenerRegistration>> locationListeners =
     70       new HashMap<>();
     71 
     72   @Implementation
     73   protected boolean isProviderEnabled(String provider) {
     74     LocationProviderEntry map = providersEnabled.get(provider);
     75     if (map != null) {
     76       Boolean isEnabled = map.getKey();
     77       return isEnabled == null ? true : isEnabled;
     78     }
     79     return false;
     80   }
     81 
     82   @Implementation
     83   protected List<String> getAllProviders() {
     84     Set<String> allKnownProviders = new LinkedHashSet<>(providersEnabled.keySet());
     85     allKnownProviders.add(LocationManager.GPS_PROVIDER);
     86     allKnownProviders.add(LocationManager.NETWORK_PROVIDER);
     87     allKnownProviders.add(LocationManager.PASSIVE_PROVIDER);
     88 
     89     return new ArrayList<>(allKnownProviders);
     90   }
     91 
     92   /**
     93    * Sets the value to return from {@link #isProviderEnabled(String)} for the given {@code provider}
     94    *
     95    * @param provider
     96    *            name of the provider whose status to set
     97    * @param isEnabled
     98    *            whether that provider should appear enabled
     99    */
    100   public void setProviderEnabled(String provider, boolean isEnabled) {
    101     setProviderEnabled(provider, isEnabled, null);
    102   }
    103 
    104   public void setProviderEnabled(String provider, boolean isEnabled, List<Criteria> criteria) {
    105     LocationProviderEntry providerEntry = providersEnabled.get(provider);
    106     if (providerEntry == null) {
    107       providerEntry = new LocationProviderEntry();
    108     }
    109     providerEntry.enabled = isEnabled;
    110     providerEntry.criteria = criteria;
    111     providersEnabled.put(provider, providerEntry);
    112     List<LocationListener> locationUpdateListeners = new ArrayList<>(getRequestLocationUpdateListeners());
    113     for (LocationListener locationUpdateListener : locationUpdateListeners) {
    114       if (isEnabled) {
    115         locationUpdateListener.onProviderEnabled(provider);
    116       } else {
    117         locationUpdateListener.onProviderDisabled(provider);
    118       }
    119     }
    120     // Send intent to notify about provider status
    121     final Intent intent = new Intent();
    122     intent.putExtra(LocationManager.KEY_PROVIDER_ENABLED, isEnabled);
    123     getContext().sendBroadcast(intent);
    124     Set<PendingIntent> requestLocationUdpatePendingIntentSet = requestLocationUdpateCriteriaPendingIntents
    125         .keySet();
    126     for (PendingIntent requestLocationUdpatePendingIntent : requestLocationUdpatePendingIntentSet) {
    127       try {
    128         requestLocationUdpatePendingIntent.send();
    129       } catch (CanceledException e) {
    130         requestLocationUdpateCriteriaPendingIntents
    131             .remove(requestLocationUdpatePendingIntent);
    132       }
    133     }
    134     // if this provider gets disabled and it was the best active provider, then it's not anymore
    135     if (provider.equals(bestEnabledProvider) && !isEnabled) {
    136       bestEnabledProvider = null;
    137     }
    138   }
    139 
    140   @Implementation
    141   protected List<String> getProviders(boolean enabledOnly) {
    142     ArrayList<String> enabledProviders = new ArrayList<>();
    143     for (String provider : getAllProviders()) {
    144       if (!enabledOnly || providersEnabled.get(provider) != null) {
    145         enabledProviders.add(provider);
    146       }
    147     }
    148     return enabledProviders;
    149   }
    150 
    151   @Implementation
    152   protected Location getLastKnownLocation(String provider) {
    153     return lastKnownLocations.get(provider);
    154   }
    155 
    156   @Implementation
    157   protected boolean addGpsStatusListener(Listener listener) {
    158     if (!gpsStatusListeners.contains(listener)) {
    159       gpsStatusListeners.add(listener);
    160     }
    161     return true;
    162   }
    163 
    164   @Implementation
    165   protected void removeGpsStatusListener(Listener listener) {
    166     gpsStatusListeners.remove(listener);
    167   }
    168 
    169   @Implementation
    170   protected String getBestProvider(Criteria criteria, boolean enabled) {
    171     lastBestProviderCriteria = criteria;
    172     lastBestProviderEnabled = enabled;
    173 
    174     if (criteria == null) {
    175       return getBestProviderWithNoCriteria(enabled);
    176     }
    177 
    178     return getBestProviderWithCriteria(criteria, enabled);
    179   }
    180 
    181   private String getBestProviderWithCriteria(Criteria criteria, boolean enabled) {
    182     List<String> providers = getProviders(enabled);
    183     int powerRequirement = criteria.getPowerRequirement();
    184     int accuracy = criteria.getAccuracy();
    185     for (String provider : providers) {
    186       LocationProviderEntry locationProviderEntry = providersEnabled.get(provider);
    187       if (locationProviderEntry == null) {
    188         continue;
    189       }
    190       List<Criteria> criteriaList = locationProviderEntry.getValue();
    191       if (criteriaList == null) {
    192         continue;
    193       }
    194       for (Criteria criteriaListItem : criteriaList) {
    195         if (criteria.equals(criteriaListItem)) {
    196           return provider;
    197         } else if (criteriaListItem.getAccuracy() == accuracy) {
    198           return provider;
    199         } else if (criteriaListItem.getPowerRequirement() == powerRequirement) {
    200           return provider;
    201         }
    202       }
    203     }
    204     // TODO: these conditions are incomplete
    205     for (String provider : providers) {
    206       if (provider.equals(LocationManager.NETWORK_PROVIDER) && (accuracy == Criteria.ACCURACY_COARSE || powerRequirement == Criteria.POWER_LOW)) {
    207         return provider;
    208       } else if (provider.equals(LocationManager.GPS_PROVIDER) && accuracy == Criteria.ACCURACY_FINE && powerRequirement != Criteria.POWER_LOW) {
    209         return provider;
    210       }
    211     }
    212 
    213     // No enabled provider found with the desired criteria, then return the the first registered provider(?)
    214     return providers.isEmpty()? null : providers.get(0);
    215   }
    216 
    217   private String getBestProviderWithNoCriteria(boolean enabled) {
    218     List<String> providers = getProviders(enabled);
    219 
    220     if (enabled && bestEnabledProvider != null) {
    221       return bestEnabledProvider;
    222     } else if (bestDisabledProvider != null) {
    223       return bestDisabledProvider;
    224     } else if (providers.contains(LocationManager.GPS_PROVIDER)) {
    225       return LocationManager.GPS_PROVIDER;
    226     } else if (providers.contains(LocationManager.NETWORK_PROVIDER)) {
    227       return LocationManager.NETWORK_PROVIDER;
    228     }
    229     return null;
    230   }
    231 
    232   // @SystemApi
    233   @Implementation(minSdk = P)
    234   protected void setLocationEnabledForUser(boolean enabled, UserHandle userHandle) {
    235     getContext().checkCallingPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS);
    236     locationEnabledForUser.put(userHandle, enabled);
    237   }
    238 
    239   // @SystemApi
    240   @Implementation(minSdk = P)
    241   protected boolean isLocationEnabledForUser(UserHandle userHandle) {
    242     Boolean result = locationEnabledForUser.get(userHandle);
    243     return result == null ? false : result;
    244   }
    245 
    246   @Implementation
    247   protected void requestLocationUpdates(
    248       String provider, long minTime, float minDistance, LocationListener listener) {
    249     addLocationListener(provider, listener, minTime, minDistance);
    250   }
    251 
    252   private void addLocationListener(String provider, LocationListener listener, long minTime, float minDistance) {
    253     List<ListenerRegistration> providerListeners = locationListeners.get(provider);
    254     if (providerListeners == null) {
    255       providerListeners = new ArrayList<>();
    256       locationListeners.put(provider, providerListeners);
    257     }
    258     removeDuplicates(listener, providerListeners);
    259     providerListeners.add(new ListenerRegistration(provider,
    260         minTime, minDistance, copyOf(getLastKnownLocation(provider)), listener));
    261 
    262   }
    263 
    264   private void removeDuplicates(LocationListener listener,
    265       List<ListenerRegistration> providerListeners) {
    266     final Iterator<ListenerRegistration> iterator = providerListeners.iterator();
    267     while (iterator.hasNext()) {
    268       if (iterator.next().listener.equals(listener)) {
    269         iterator.remove();
    270       }
    271     }
    272   }
    273 
    274   @Implementation
    275   protected void requestLocationUpdates(
    276       String provider, long minTime, float minDistance, LocationListener listener, Looper looper) {
    277     addLocationListener(provider, listener, minTime, minDistance);
    278   }
    279 
    280   @Implementation
    281   protected void requestLocationUpdates(
    282       long minTime, float minDistance, Criteria criteria, PendingIntent pendingIntent) {
    283     if (pendingIntent == null) {
    284       throw new IllegalStateException("Intent must not be null");
    285     }
    286     if (getBestProvider(criteria, true) == null) {
    287       throw new IllegalArgumentException("no providers found for criteria");
    288     }
    289     requestLocationUdpateCriteriaPendingIntents.put(pendingIntent, criteria);
    290   }
    291 
    292   @Implementation
    293   protected void requestLocationUpdates(
    294       String provider, long minTime, float minDistance, PendingIntent pendingIntent) {
    295     if (pendingIntent == null) {
    296       throw new IllegalStateException("Intent must not be null");
    297     }
    298     if (!providersEnabled.containsKey(provider)) {
    299       throw new IllegalArgumentException("no providers found");
    300     }
    301 
    302     requestLocationUdpateProviderPendingIntents.put(pendingIntent, provider);
    303   }
    304 
    305   @Implementation
    306   protected void removeUpdates(LocationListener listener) {
    307     removedLocationListeners.add(listener);
    308   }
    309 
    310   private void cleanupRemovedLocationListeners() {
    311     for (Map.Entry<String, List<ListenerRegistration>> entry : locationListeners.entrySet()) {
    312       List<ListenerRegistration> listenerRegistrations = entry.getValue();
    313       for (int i = listenerRegistrations.size() - 1; i >= 0; i--) {
    314         LocationListener listener = listenerRegistrations.get(i).listener;
    315         if(removedLocationListeners.contains(listener)) {
    316           listenerRegistrations.remove(i);
    317         }
    318       }
    319     }
    320   }
    321 
    322   @Implementation
    323   protected void removeUpdates(PendingIntent pendingIntent) {
    324     while (requestLocationUdpateCriteriaPendingIntents.remove(pendingIntent) != null);
    325     while (requestLocationUdpateProviderPendingIntents.remove(pendingIntent) != null);
    326   }
    327 
    328   public boolean hasGpsStatusListener(Listener listener) {
    329     return gpsStatusListeners.contains(listener);
    330   }
    331 
    332   /**
    333    * Gets the criteria value used in the last call to {@link #getBestProvider(android.location.Criteria, boolean)}.
    334    *
    335    * @return the criteria used to find the best provider
    336    */
    337   public Criteria getLastBestProviderCriteria() {
    338     return lastBestProviderCriteria;
    339   }
    340 
    341   /**
    342    * Gets the enabled value used in the last call to {@link #getBestProvider(android.location.Criteria, boolean)}
    343    *
    344    * @return the enabled value used to find the best provider
    345    */
    346   public boolean getLastBestProviderEnabledOnly() {
    347     return lastBestProviderEnabled;
    348   }
    349 
    350   /**
    351    * Sets the value to return from {@link #getBestProvider(android.location.Criteria, boolean)} for the given
    352    * {@code provider}
    353    *
    354    * @param provider name of the provider who should be considered best
    355    * @param enabled Enabled
    356    * @param criteria List of criteria
    357    * @throws Exception if provider is not known
    358    * @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
    359    */
    360   public boolean setBestProvider(String provider, boolean enabled, List<Criteria> criteria) throws Exception {
    361     if (!getAllProviders().contains(provider)) {
    362       throw new IllegalStateException("Best provider is not a known provider");
    363     }
    364     // If provider is not enabled but it is supposed to be set as the best enabled provider don't set it.
    365     for (String prvdr : providersEnabled.keySet()) {
    366       if (provider.equals(prvdr) && providersEnabled.get(prvdr).enabled != enabled) {
    367         return false;
    368       }
    369     }
    370 
    371     if (enabled) {
    372       bestEnabledProvider = provider;
    373       if (provider.equals(bestDisabledProvider)) {
    374         bestDisabledProvider = null;
    375       }
    376     } else {
    377       bestDisabledProvider = provider;
    378       if (provider.equals(bestEnabledProvider)) {
    379         bestEnabledProvider = null;
    380       }
    381     }
    382     if (criteria == null) {
    383       return true;
    384     }
    385     LocationProviderEntry entry;
    386     if (!providersEnabled.containsKey(provider)) {
    387       entry = new LocationProviderEntry();
    388       entry.enabled = enabled;
    389       entry.criteria = criteria;
    390     } else {
    391       entry = providersEnabled.get(provider);
    392     }
    393     providersEnabled.put(provider, entry);
    394 
    395     return true;
    396   }
    397 
    398   public boolean setBestProvider(String provider, boolean enabled) throws Exception {
    399     return setBestProvider(provider, enabled, null);
    400   }
    401 
    402   /**
    403    * Sets the value to return from {@link #getLastKnownLocation(String)} for the given {@code provider}
    404    *
    405    * @param provider
    406    *            name of the provider whose location to set
    407    * @param location
    408    *            the last known location for the provider
    409    */
    410   public void setLastKnownLocation(String provider, Location location) {
    411     lastKnownLocations.put(provider, location);
    412   }
    413 
    414   /**
    415    * @return lastRequestedLocationUpdatesLocationListener
    416    */
    417   public List<LocationListener> getRequestLocationUpdateListeners() {
    418     cleanupRemovedLocationListeners();
    419     List<LocationListener> all = new ArrayList<>();
    420     for (Map.Entry<String, List<ListenerRegistration>> entry : locationListeners.entrySet()) {
    421       for (ListenerRegistration reg : entry.getValue()) {
    422         all.add(reg.listener);
    423       }
    424     }
    425 
    426     return all;
    427   }
    428 
    429   public void simulateLocation(Location location) {
    430     cleanupRemovedLocationListeners();
    431     setLastKnownLocation(location.getProvider(), location);
    432 
    433     List<ListenerRegistration> providerListeners = locationListeners.get(
    434         location.getProvider());
    435     if (providerListeners == null) return;
    436 
    437     for (ListenerRegistration listenerReg : providerListeners) {
    438       if(listenerReg.lastSeenLocation != null && location != null) {
    439         float distanceChange = distanceBetween(location, listenerReg.lastSeenLocation);
    440         boolean withinMinDistance = distanceChange < listenerReg.minDistance;
    441         boolean exceededMinTime = location.getTime() - listenerReg.lastSeenTime > listenerReg.minTime;
    442         if (withinMinDistance || !exceededMinTime) continue;
    443       }
    444       listenerReg.lastSeenLocation = copyOf(location);
    445       listenerReg.lastSeenTime = location == null ? 0 : location.getTime();
    446       listenerReg.listener.onLocationChanged(copyOf(location));
    447     }
    448     cleanupRemovedLocationListeners();
    449   }
    450 
    451   private Location copyOf(Location location) {
    452     if (location == null) return null;
    453     Location copy = new Location(location);
    454     copy.setAccuracy(location.getAccuracy());
    455     copy.setAltitude(location.getAltitude());
    456     copy.setBearing(location.getBearing());
    457     copy.setExtras(location.getExtras());
    458     copy.setLatitude(location.getLatitude());
    459     copy.setLongitude(location.getLongitude());
    460     copy.setProvider(location.getProvider());
    461     copy.setSpeed(location.getSpeed());
    462     copy.setTime(location.getTime());
    463     return copy;
    464   }
    465 
    466   /**
    467    * Returns the distance between the two locations in meters.
    468    * Adapted from: http://stackoverflow.com/questions/837872/calculate-distance-in-meters-when-you-know-longitude-and-latitude-in-java
    469    */
    470   private static float distanceBetween(Location location1, Location location2) {
    471     double earthRadius = 3958.75;
    472     double latDifference = Math.toRadians(location2.getLatitude() - location1.getLatitude());
    473     double lonDifference = Math.toRadians(location2.getLongitude() - location1.getLongitude());
    474     double a = Math.sin(latDifference/2) * Math.sin(latDifference/2) +
    475         Math.cos(Math.toRadians(location1.getLatitude())) * Math.cos(Math.toRadians(location2.getLatitude())) *
    476             Math.sin(lonDifference/2) * Math.sin(lonDifference/2);
    477     double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    478     double dist = Math.abs(earthRadius * c);
    479 
    480     int meterConversion = 1609;
    481 
    482     return (float) (dist * meterConversion);
    483   }
    484 
    485   public Map<PendingIntent, Criteria> getRequestLocationUdpateCriteriaPendingIntents() {
    486     return requestLocationUdpateCriteriaPendingIntents;
    487   }
    488 
    489   public Map<PendingIntent, String> getRequestLocationUdpateProviderPendingIntents() {
    490     return requestLocationUdpateProviderPendingIntents;
    491   }
    492 
    493   public Collection<String> getProvidersForListener(LocationListener listener) {
    494     cleanupRemovedLocationListeners();
    495     Set<String> providers = new HashSet<>();
    496     for (List<ListenerRegistration> listenerRegistrations : locationListeners.values()) {
    497       for (ListenerRegistration listenerRegistration : listenerRegistrations) {
    498         if (listenerRegistration.listener == listener) {
    499           providers.add(listenerRegistration.provider);
    500         }
    501       }
    502     }
    503     return providers;
    504   }
    505 
    506   final private static class LocationProviderEntry implements Map.Entry<Boolean, List<Criteria>> {
    507     private Boolean enabled;
    508     private List<Criteria> criteria;
    509 
    510     @Override
    511     public Boolean getKey() {
    512       return enabled;
    513     }
    514 
    515     @Override
    516     public List<Criteria> getValue() {
    517       return criteria;
    518     }
    519 
    520     @Override
    521     public List<Criteria> setValue(List<Criteria> criteria) {
    522       List<Criteria> oldCriteria = this.criteria;
    523       this.criteria = criteria;
    524       return oldCriteria;
    525     }
    526   }
    527 
    528   private Context getContext() {
    529     return ReflectionHelpers.getField(realLocationManager, "mContext");
    530   }
    531 }
    532