• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
ListenerRegistration(String provider, long minTime, float minDistance, Location locationAtCreation, LocationListener listener)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
isProviderEnabled(String provider)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
getAllProviders()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    */
setProviderEnabled(String provider, boolean isEnabled)100   public void setProviderEnabled(String provider, boolean isEnabled) {
101     setProviderEnabled(provider, isEnabled, null);
102   }
103 
setProviderEnabled(String provider, boolean isEnabled, List<Criteria> criteria)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
getProviders(boolean enabledOnly)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
getLastKnownLocation(String provider)152   protected Location getLastKnownLocation(String provider) {
153     return lastKnownLocations.get(provider);
154   }
155 
156   @Implementation
addGpsStatusListener(Listener listener)157   protected boolean addGpsStatusListener(Listener listener) {
158     if (!gpsStatusListeners.contains(listener)) {
159       gpsStatusListeners.add(listener);
160     }
161     return true;
162   }
163 
164   @Implementation
removeGpsStatusListener(Listener listener)165   protected void removeGpsStatusListener(Listener listener) {
166     gpsStatusListeners.remove(listener);
167   }
168 
169   @Implementation
getBestProvider(Criteria criteria, boolean enabled)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 
getBestProviderWithCriteria(Criteria criteria, boolean enabled)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 
getBestProviderWithNoCriteria(boolean enabled)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)
setLocationEnabledForUser(boolean enabled, UserHandle userHandle)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)
isLocationEnabledForUser(UserHandle userHandle)241   protected boolean isLocationEnabledForUser(UserHandle userHandle) {
242     Boolean result = locationEnabledForUser.get(userHandle);
243     return result == null ? false : result;
244   }
245 
246   @Implementation
requestLocationUpdates( String provider, long minTime, float minDistance, LocationListener listener)247   protected void requestLocationUpdates(
248       String provider, long minTime, float minDistance, LocationListener listener) {
249     addLocationListener(provider, listener, minTime, minDistance);
250   }
251 
addLocationListener(String provider, LocationListener listener, long minTime, float minDistance)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 
removeDuplicates(LocationListener listener, List<ListenerRegistration> providerListeners)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
requestLocationUpdates( String provider, long minTime, float minDistance, LocationListener listener, Looper looper)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
requestLocationUpdates( long minTime, float minDistance, Criteria criteria, PendingIntent pendingIntent)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
requestLocationUpdates( String provider, long minTime, float minDistance, PendingIntent pendingIntent)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
removeUpdates(LocationListener listener)306   protected void removeUpdates(LocationListener listener) {
307     removedLocationListeners.add(listener);
308   }
309 
cleanupRemovedLocationListeners()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
removeUpdates(PendingIntent pendingIntent)323   protected void removeUpdates(PendingIntent pendingIntent) {
324     while (requestLocationUdpateCriteriaPendingIntents.remove(pendingIntent) != null);
325     while (requestLocationUdpateProviderPendingIntents.remove(pendingIntent) != null);
326   }
327 
hasGpsStatusListener(Listener listener)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    */
getLastBestProviderCriteria()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    */
getLastBestProviderEnabledOnly()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    */
setBestProvider(String provider, boolean enabled, List<Criteria> criteria)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 
setBestProvider(String provider, boolean enabled)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    */
setLastKnownLocation(String provider, Location location)410   public void setLastKnownLocation(String provider, Location location) {
411     lastKnownLocations.put(provider, location);
412   }
413 
414   /**
415    * @return lastRequestedLocationUpdatesLocationListener
416    */
getRequestLocationUpdateListeners()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 
simulateLocation(Location location)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 
copyOf(Location location)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    */
distanceBetween(Location location1, Location location2)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 
getRequestLocationUdpateCriteriaPendingIntents()485   public Map<PendingIntent, Criteria> getRequestLocationUdpateCriteriaPendingIntents() {
486     return requestLocationUdpateCriteriaPendingIntents;
487   }
488 
getRequestLocationUdpateProviderPendingIntents()489   public Map<PendingIntent, String> getRequestLocationUdpateProviderPendingIntents() {
490     return requestLocationUdpateProviderPendingIntents;
491   }
492 
getProvidersForListener(LocationListener listener)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
getKey()511     public Boolean getKey() {
512       return enabled;
513     }
514 
515     @Override
getValue()516     public List<Criteria> getValue() {
517       return criteria;
518     }
519 
520     @Override
setValue(List<Criteria> criteria)521     public List<Criteria> setValue(List<Criteria> criteria) {
522       List<Criteria> oldCriteria = this.criteria;
523       this.criteria = criteria;
524       return oldCriteria;
525     }
526   }
527 
getContext()528   private Context getContext() {
529     return ReflectionHelpers.getField(realLocationManager, "mContext");
530   }
531 }
532