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