1 package com.xtremelabs.robolectric.shadows;
2 
3 import android.app.PendingIntent;
4 import android.app.PendingIntent.CanceledException;
5 import android.content.Intent;
6 import android.location.Criteria;
7 import android.location.GpsStatus.Listener;
8 import android.location.Location;
9 import android.location.LocationListener;
10 import android.location.LocationManager;
11 import android.os.Looper;
12 import com.xtremelabs.robolectric.Robolectric;
13 import com.xtremelabs.robolectric.internal.Implementation;
14 import com.xtremelabs.robolectric.internal.Implements;
15 
16 import java.util.*;
17 
18 /**
19  * Shadow of {@code LocationManager} that provides for the simulation of different location providers being enabled and
20  * disabled.
21  */
22 @Implements(LocationManager.class)
23 public class ShadowLocationManager {
24     private final Map<String, LocationProviderEntry> providersEnabled = new LinkedHashMap<String, LocationProviderEntry>();
25     private final Map<String, Location> lastKnownLocations = new HashMap<String, Location>();
26     private final Map<PendingIntent, Criteria> requestLocationUdpateCriteriaPendingIntents = new HashMap<PendingIntent, Criteria>();
27     private final Map<PendingIntent, String> requestLocationUdpateProviderPendingIntents = new HashMap<PendingIntent, String>();
28 
29     private final ArrayList<Listener> gpsStatusListeners = new ArrayList<Listener>();
30     private Criteria lastBestProviderCriteria;
31     private boolean lastBestProviderEnabled;
32     private String bestEnabledProvider, bestDisabledProvider;
33     private final Map<LocationListener, Set<String>> requestLocationUpdateListenersMap = new LinkedHashMap<LocationListener, Set<String>>();
34 
35     @Implementation
isProviderEnabled(String provider)36     public boolean isProviderEnabled(String provider) {
37         LocationProviderEntry map = providersEnabled.get(provider);
38         if (map != null) {
39             Boolean isEnabled = map.getKey();
40             return isEnabled == null ? true : isEnabled;
41         }
42         return false;
43     }
44 
45     @Implementation
getAllProviders()46     public List<String> getAllProviders() {
47         Set<String> allKnownProviders = new LinkedHashSet<String>(providersEnabled.keySet());
48         allKnownProviders.add(LocationManager.GPS_PROVIDER);
49         allKnownProviders.add(LocationManager.NETWORK_PROVIDER);
50         allKnownProviders.add(LocationManager.PASSIVE_PROVIDER);
51 
52         return new ArrayList<String>(allKnownProviders);
53     }
54 
55     /**
56      * Sets the value to return from {@link #isProviderEnabled(String)} for the given {@code provider}
57      *
58      * @param provider
59      *            name of the provider whose status to set
60      * @param isEnabled
61      *            whether that provider should appear enabled
62      */
setProviderEnabled(String provider, boolean isEnabled)63     public void setProviderEnabled(String provider, boolean isEnabled) {
64         setProviderEnabled(provider, isEnabled, null);
65     }
66 
setProviderEnabled(String provider, boolean isEnabled, List<Criteria> criteria)67     public void setProviderEnabled(String provider, boolean isEnabled, List<Criteria> criteria) {
68         LocationProviderEntry providerEntry = providersEnabled.get(provider);
69         if (providerEntry == null) {
70             providerEntry = new LocationProviderEntry();
71         }
72         providerEntry.enabled = isEnabled;
73         providerEntry.criteria = criteria;
74         providersEnabled.put(provider, providerEntry);
75         List<LocationListener> locationUpdateListeners = new ArrayList<LocationListener>(requestLocationUpdateListenersMap.keySet());
76         for (LocationListener locationUpdateListener : locationUpdateListeners) {
77             if (isEnabled) {
78                 locationUpdateListener.onProviderEnabled(provider);
79             } else {
80                 locationUpdateListener.onProviderDisabled(provider);
81             }
82         }
83         // Send intent to notify about provider status
84         final Intent intent = new Intent();
85         intent.putExtra(LocationManager.KEY_PROVIDER_ENABLED, isEnabled);
86         Robolectric.getShadowApplication().sendBroadcast(intent);
87         Set<PendingIntent> requestLocationUdpatePendingIntentSet = requestLocationUdpateCriteriaPendingIntents
88                 .keySet();
89         for (PendingIntent requestLocationUdpatePendingIntent : requestLocationUdpatePendingIntentSet) {
90             try {
91                 requestLocationUdpatePendingIntent.send();
92             } catch (CanceledException e) {
93                 requestLocationUdpateCriteriaPendingIntents
94                         .remove(requestLocationUdpatePendingIntent);
95             }
96         }
97         // if this provider gets disabled and it was the best active provider, then it's not anymore
98         if (provider.equals(bestEnabledProvider) && !isEnabled) {
99             bestEnabledProvider = null;
100         }
101     }
102 
103     @Implementation
getProviders(boolean enabledOnly)104     public List<String> getProviders(boolean enabledOnly) {
105         ArrayList<String> enabledProviders = new ArrayList<String>();
106         for (String provider : providersEnabled.keySet()) {
107             if (!enabledOnly || providersEnabled.get(provider).getKey()) {
108                 enabledProviders.add(provider);
109             }
110         }
111         return enabledProviders;
112     }
113 
114     @Implementation
getLastKnownLocation(String provider)115     public Location getLastKnownLocation(String provider) {
116         return lastKnownLocations.get(provider);
117     }
118 
119     @Implementation
addGpsStatusListener(Listener listener)120     public boolean addGpsStatusListener(Listener listener) {
121         if (!gpsStatusListeners.contains(listener)) {
122             gpsStatusListeners.add(listener);
123         }
124         return true;
125     }
126 
127     @Implementation
removeGpsStatusListener(Listener listener)128     public void removeGpsStatusListener(Listener listener) {
129         gpsStatusListeners.remove(listener);
130     }
131 
132     /**
133      * Returns the best provider with respect to the passed criteria (if any) and its status. If no criteria are passed
134      *
135      * NB: Gps is considered the best provider for fine accuracy and high power consumption, network is considered the
136      * best provider for coarse accuracy and low power consumption.
137      *
138      * @param criteria
139      * @param enabled
140      * @return
141      */
142     @Implementation
getBestProvider(Criteria criteria, boolean enabled)143     public String getBestProvider(Criteria criteria, boolean enabled) {
144         lastBestProviderCriteria = criteria;
145         lastBestProviderEnabled = enabled;
146 
147         if (criteria == null) {
148             return getBestProviderWithNoCriteria(enabled);
149         }
150 
151         return getBestProviderWithCriteria(criteria, enabled);
152     }
153 
getBestProviderWithCriteria(Criteria criteria, boolean enabled)154     private String getBestProviderWithCriteria(Criteria criteria, boolean enabled) {
155         List<String> providers = getProviders(enabled);
156         int powerRequirement = criteria.getPowerRequirement();
157         int accuracy = criteria.getAccuracy();
158         for (String provider : providers) {
159             LocationProviderEntry locationProviderEntry = providersEnabled.get(provider);
160             if (locationProviderEntry == null) {
161                 continue;
162             }
163             List<Criteria> criteriaList = locationProviderEntry.getValue();
164             if (criteriaList == null) {
165                 continue;
166             }
167             for (Criteria criteriaListItem : criteriaList) {
168                 if (criteria.equals(criteriaListItem)) {
169                     return provider;
170                 } else if (criteriaListItem.getAccuracy() == accuracy) {
171                     return provider;
172                 } else if (criteriaListItem.getPowerRequirement() == powerRequirement) {
173                     return provider;
174                 }
175             }
176         }
177         // TODO: these conditions are incomplete
178         for (String provider : providers) {
179             if (provider.equals(LocationManager.NETWORK_PROVIDER) && (accuracy == Criteria.ACCURACY_COARSE || powerRequirement == Criteria.POWER_LOW)) {
180                 return provider;
181             } else if (provider.equals(LocationManager.GPS_PROVIDER) && accuracy == Criteria.ACCURACY_FINE && powerRequirement != Criteria.POWER_LOW) {
182                 return provider;
183             }
184         }
185 
186         // No enabled provider found with the desired criteria, then return the the first registered provider(?)
187         return providers.isEmpty()? null : providers.get(0);
188     }
189 
getBestProviderWithNoCriteria(boolean enabled)190     private String getBestProviderWithNoCriteria(boolean enabled) {
191         List<String> providers = getProviders(enabled);
192 
193         if (enabled && bestEnabledProvider != null) {
194             return bestEnabledProvider;
195         } else if (bestDisabledProvider != null) {
196             return bestDisabledProvider;
197         } else if (providers.contains(LocationManager.GPS_PROVIDER)) {
198             return LocationManager.GPS_PROVIDER;
199         } else if (providers.contains(LocationManager.NETWORK_PROVIDER)) {
200             return LocationManager.NETWORK_PROVIDER;
201         }
202         return null;
203     }
204 
205     @Implementation
requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener)206     public void requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener) {
207         addLocationListener(provider, listener);
208     }
209 
addLocationListener(String provider, LocationListener listener)210     private void addLocationListener(String provider, LocationListener listener) {
211         if (!requestLocationUpdateListenersMap.containsKey(listener)) {
212             requestLocationUpdateListenersMap.put(listener, new HashSet<String>());
213         }
214         requestLocationUpdateListenersMap.get(listener).add(provider);
215     }
216 
217     @Implementation
requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener, Looper looper)218     public void requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener,
219             Looper looper) {
220         addLocationListener(provider, listener);
221     }
222 
223     @Implementation
requestLocationUpdates(long minTime, float minDistance, Criteria criteria, PendingIntent pendingIntent)224     public void requestLocationUpdates(long minTime, float minDistance, Criteria criteria, PendingIntent pendingIntent) {
225         if (pendingIntent == null) {
226             throw new IllegalStateException("Intent must not be null");
227         }
228         if (getBestProvider(criteria, true) == null) {
229             throw new IllegalArgumentException("no providers found for criteria");
230         }
231         requestLocationUdpateCriteriaPendingIntents.put(pendingIntent, criteria);
232     }
233 
234     @Implementation
requestLocationUpdates(String provider, long minTime, float minDistance, PendingIntent pendingIntent)235     public void requestLocationUpdates(String provider, long minTime, float minDistance,
236             PendingIntent pendingIntent) {
237         if (pendingIntent == null) {
238             throw new IllegalStateException("Intent must not be null");
239         }
240         if (!providersEnabled.containsKey(provider)) {
241             throw new IllegalArgumentException("no providers found");
242         }
243 
244         requestLocationUdpateProviderPendingIntents.put(pendingIntent, provider);
245     }
246 
247     @Implementation
removeUpdates(LocationListener listener)248     public void removeUpdates(LocationListener listener) {
249         requestLocationUpdateListenersMap.remove(listener);
250     }
251 
252     @Implementation
removeUpdates(PendingIntent pendingIntent)253     public void removeUpdates(PendingIntent pendingIntent) {
254         while (requestLocationUdpateCriteriaPendingIntents.remove(pendingIntent) != null);
255         while (requestLocationUdpateProviderPendingIntents.remove(pendingIntent) != null);
256     }
257 
hasGpsStatusListener(Listener listener)258     public boolean hasGpsStatusListener(Listener listener) {
259         return gpsStatusListeners.contains(listener);
260     }
261 
262     /**
263      * Non-Android accessor.
264      * <p/>
265      * Gets the criteria value used in the last call to {@link #getBestProvider(android.location.Criteria, boolean)}
266      *
267      * @return the criteria used to find the best provider
268      */
getLastBestProviderCriteria()269     public Criteria getLastBestProviderCriteria() {
270         return lastBestProviderCriteria;
271     }
272 
273     /**
274      * Non-Android accessor.
275      * <p/>
276      * Gets the enabled value used in the last call to {@link #getBestProvider(android.location.Criteria, boolean)}
277      *
278      * @return the enabled value used to find the best provider
279      */
getLastBestProviderEnabledOnly()280     public boolean getLastBestProviderEnabledOnly() {
281         return lastBestProviderEnabled;
282     }
283 
284     /**
285      * Sets the value to return from {@link #getBestProvider(android.location.Criteria, boolean)} for the given
286      * {@code provider}
287      *
288      * @param provider
289      *            name of the provider who should be considered best
290      * @throws Exception
291      *
292      */
setBestProvider(String provider, boolean enabled, List<Criteria> criteria)293     public boolean setBestProvider(String provider, boolean enabled, List<Criteria> criteria) throws Exception {
294         if (!getAllProviders().contains(provider)) {
295             throw new IllegalStateException("Best provider is not a known provider");
296         }
297         // If provider is not enabled but it is supposed to be set as the best enabled provider don't set it.
298         for (String prvdr : providersEnabled.keySet()) {
299             if (provider.equals(prvdr) && providersEnabled.get(prvdr).enabled != enabled) {
300                 return false;
301             }
302         }
303 
304         if (enabled) {
305             bestEnabledProvider = provider;
306             if (provider.equals(bestDisabledProvider)) {
307                 bestDisabledProvider = null;
308             }
309         } else {
310             bestDisabledProvider = provider;
311             if (provider.equals(bestEnabledProvider)) {
312                 bestEnabledProvider = null;
313             }
314         }
315         if (criteria == null) {
316             return true;
317         }
318         LocationProviderEntry entry;
319         if (!providersEnabled.containsKey(provider)) {
320             entry = new LocationProviderEntry();
321             entry.enabled = enabled;
322             entry.criteria = criteria;
323         } else {
324             entry = providersEnabled.get(provider);
325         }
326         providersEnabled.put(provider, entry);
327 
328         return true;
329     }
330 
setBestProvider(String provider, boolean enabled)331     public boolean setBestProvider(String provider, boolean enabled) throws Exception {
332         return setBestProvider(provider, enabled, null);
333     }
334 
335     /**
336      * Sets the value to return from {@link #getLastKnownLocation(String)} for the given {@code provider}
337      *
338      * @param provider
339      *            name of the provider whose location to set
340      * @param location
341      *            the last known location for the provider
342      */
setLastKnownLocation(String provider, Location location)343     public void setLastKnownLocation(String provider, Location location) {
344         lastKnownLocations.put(provider, location);
345     }
346 
347     /**
348      * Non-Android accessor.
349      *
350      * @return lastRequestedLocationUpdatesLocationListener
351      */
getRequestLocationUpdateListeners()352     public List<LocationListener> getRequestLocationUpdateListeners() {
353         return new ArrayList<LocationListener>(requestLocationUpdateListenersMap.keySet());
354     }
355 
getRequestLocationUdpateCriteriaPendingIntents()356     public Map<PendingIntent, Criteria> getRequestLocationUdpateCriteriaPendingIntents() {
357         return requestLocationUdpateCriteriaPendingIntents;
358     }
359 
getRequestLocationUdpateProviderPendingIntents()360     public Map<PendingIntent, String> getRequestLocationUdpateProviderPendingIntents() {
361         return requestLocationUdpateProviderPendingIntents;
362     }
363 
getProvidersForListener(LocationListener listener)364     public Collection<String> getProvidersForListener(LocationListener listener) {
365         Set<String> providers = requestLocationUpdateListenersMap.get(listener);
366         return providers == null ? Collections.<String>emptyList() : new ArrayList<String>(providers);
367     }
368 
369     final private class LocationProviderEntry implements Map.Entry<Boolean, List<Criteria>> {
370         private Boolean enabled;
371         private List<Criteria> criteria;
372 
373         @Override
getKey()374         public Boolean getKey() {
375             return enabled;
376         }
377 
378         @Override
getValue()379         public List<Criteria> getValue() {
380             return criteria;
381         }
382 
383         @Override
setValue(List<Criteria> criteria)384         public List<Criteria> setValue(List<Criteria> criteria) {
385             List<Criteria> oldCriteria = this.criteria;
386             this.criteria = criteria;
387             return oldCriteria;
388         }
389     }
390 
391 }
392