1package com.xtremelabs.robolectric.shadows;
2
3import android.app.PendingIntent;
4import android.app.PendingIntent.CanceledException;
5import android.content.Intent;
6import android.location.Criteria;
7import android.location.GpsStatus.Listener;
8import android.location.Location;
9import android.location.LocationListener;
10import android.location.LocationManager;
11import android.os.Looper;
12import com.xtremelabs.robolectric.Robolectric;
13import com.xtremelabs.robolectric.internal.Implementation;
14import com.xtremelabs.robolectric.internal.Implements;
15
16import 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)
23public 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
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
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     */
63    public void setProviderEnabled(String provider, boolean isEnabled) {
64        setProviderEnabled(provider, isEnabled, null);
65    }
66
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
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
115    public Location getLastKnownLocation(String provider) {
116        return lastKnownLocations.get(provider);
117    }
118
119    @Implementation
120    public boolean addGpsStatusListener(Listener listener) {
121        if (!gpsStatusListeners.contains(listener)) {
122            gpsStatusListeners.add(listener);
123        }
124        return true;
125    }
126
127    @Implementation
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
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
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
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
206    public void requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener) {
207        addLocationListener(provider, listener);
208    }
209
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
218    public void requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener,
219            Looper looper) {
220        addLocationListener(provider, listener);
221    }
222
223    @Implementation
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
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
248    public void removeUpdates(LocationListener listener) {
249        requestLocationUpdateListenersMap.remove(listener);
250    }
251
252    @Implementation
253    public void removeUpdates(PendingIntent pendingIntent) {
254        while (requestLocationUdpateCriteriaPendingIntents.remove(pendingIntent) != null);
255        while (requestLocationUdpateProviderPendingIntents.remove(pendingIntent) != null);
256    }
257
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     */
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     */
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     */
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
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     */
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     */
352    public List<LocationListener> getRequestLocationUpdateListeners() {
353        return new ArrayList<LocationListener>(requestLocationUpdateListenersMap.keySet());
354    }
355
356    public Map<PendingIntent, Criteria> getRequestLocationUdpateCriteriaPendingIntents() {
357        return requestLocationUdpateCriteriaPendingIntents;
358    }
359
360    public Map<PendingIntent, String> getRequestLocationUdpateProviderPendingIntents() {
361        return requestLocationUdpateProviderPendingIntents;
362    }
363
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
374        public Boolean getKey() {
375            return enabled;
376        }
377
378        @Override
379        public List<Criteria> getValue() {
380            return criteria;
381        }
382
383        @Override
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