diff --git a/lost/src/main/java/com/mapzen/android/lost/api/LocationServices.java b/lost/src/main/java/com/mapzen/android/lost/api/LocationServices.java index d4fdad4..19155e0 100644 --- a/lost/src/main/java/com/mapzen/android/lost/api/LocationServices.java +++ b/lost/src/main/java/com/mapzen/android/lost/api/LocationServices.java @@ -2,6 +2,7 @@ import com.mapzen.android.lost.internal.DwellServiceIntentFactory; import com.mapzen.android.lost.internal.FusedLocationProviderApiImpl; +import com.mapzen.android.lost.internal.FusedLocationServiceCallbackManager; import com.mapzen.android.lost.internal.FusedLocationServiceConnectionManager; import com.mapzen.android.lost.internal.GeofencingApiImpl; import com.mapzen.android.lost.internal.GeofencingServiceIntentFactory; @@ -17,7 +18,8 @@ public class LocationServices { * Entry point for APIs concerning location updates. */ public static final FusedLocationProviderApi FusedLocationApi = - new FusedLocationProviderApiImpl(new FusedLocationServiceConnectionManager()); + new FusedLocationProviderApiImpl(new FusedLocationServiceConnectionManager(), + new FusedLocationServiceCallbackManager()); /** * Entry point for APIs concerning geofences. diff --git a/lost/src/main/java/com/mapzen/android/lost/api/LostApiClient.java b/lost/src/main/java/com/mapzen/android/lost/api/LostApiClient.java index 7bf94ca..7db8cc1 100644 --- a/lost/src/main/java/com/mapzen/android/lost/api/LostApiClient.java +++ b/lost/src/main/java/com/mapzen/android/lost/api/LostApiClient.java @@ -14,27 +14,63 @@ interface ConnectionCallbacks { void onConnectionSuspended(); } + /** + * Connects the client so that it will be ready for use. This must be done before any of the + * {@link LocationServices} APIs can be interacted with. When the client is connected, any + * registered {@link ConnectionCallbacks} will receive a call to + * {@link ConnectionCallbacks#onConnected()} and the client can then be used. + */ void connect(); + /** + * Disconnects the client. To avoid {@link IllegalStateException}s, be sure to unregister any + * location updates requested through the {@link FusedLocationProviderApi}. + */ void disconnect(); + /** + * Returns whether or not the client is connected and ready to be used. + * @return + */ boolean isConnected(); + /** + * Unregisters callbacks added in {@link LostApiClient.Builder#addConnectionCallbacks( + * ConnectionCallbacks)}. Use this method to avoid leaking resources. + * @param callbacks + */ void unregisterConnectionCallbacks(ConnectionCallbacks callbacks); + /** + * {@link LostApiClient} builder class for creating and configuring new instances. + */ final class Builder { private final Context context; private WeakReference connectionCallbacks; + /** + * Creates a new object using the {@link Context}'s application context. + * @param context + */ public Builder(Context context) { this.context = context.getApplicationContext(); } + /** + * Adds {@link ConnectionCallbacks} to the client. It is strongly recommended that these + * callbacks are used to determine when the client is connected and ready for use. + * @param callbacks + * @return + */ public Builder addConnectionCallbacks(ConnectionCallbacks callbacks) { this.connectionCallbacks = new WeakReference(callbacks); return this; } + /** + * Builds a new client given the properties currently configured on the builder. + * @return + */ public LostApiClient build() { ConnectionCallbacks callbacks = null; if (connectionCallbacks != null) { diff --git a/lost/src/main/java/com/mapzen/android/lost/internal/FusedLocationProviderApiImpl.java b/lost/src/main/java/com/mapzen/android/lost/internal/FusedLocationProviderApiImpl.java index 32ed035..8ebfe6c 100644 --- a/lost/src/main/java/com/mapzen/android/lost/internal/FusedLocationProviderApiImpl.java +++ b/lost/src/main/java/com/mapzen/android/lost/internal/FusedLocationProviderApiImpl.java @@ -5,7 +5,6 @@ import com.mapzen.android.lost.api.LocationCallback; import com.mapzen.android.lost.api.LocationListener; import com.mapzen.android.lost.api.LocationRequest; -import com.mapzen.android.lost.api.LocationResult; import com.mapzen.android.lost.api.LostApiClient; import com.mapzen.android.lost.api.PendingResult; import com.mapzen.android.lost.api.Status; @@ -22,7 +21,6 @@ import android.os.Looper; import android.os.RemoteException; -import java.util.ArrayList; import java.util.Map; import java.util.Set; @@ -34,6 +32,7 @@ public class FusedLocationProviderApiImpl extends ApiImpl private Context context; private FusedLocationServiceConnectionManager serviceConnectionManager; + private FusedLocationServiceCallbackManager serviceCallbackManager; private boolean isBound; IFusedLocationProviderService service; @@ -44,33 +43,15 @@ public void onLocationChanged(final Location location) throws RemoteException { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { final LostClientManager clientManager = LostClientManager.shared(); - ReportedChanges changes = clientManager.reportLocationChanged(location); - - LocationAvailability availability; - try { - availability = service.getLocationAvailability(); - } catch (RemoteException e) { - throw new RuntimeException(e); - } - - ArrayList locations = new ArrayList<>(); - locations.add(location); - final LocationResult result = LocationResult.create(locations); - ReportedChanges pendingIntentChanges = clientManager.sendPendingIntent( - context, location, availability, result); - - ReportedChanges callbackChanges = clientManager.reportLocationResult(location, result); - - changes.putAll(pendingIntentChanges); - changes.putAll(callbackChanges); - clientManager.updateReportedValues(changes); + serviceCallbackManager.onLocationChanged(context, location, clientManager, service); } }); } @Override public void onLocationAvailabilityChanged(LocationAvailability locationAvailability) throws RemoteException { - LostClientManager.shared().notifyLocationAvailability(locationAvailability); + LostClientManager clientManager = LostClientManager.shared(); + serviceCallbackManager.onLocationAvailabilityChanged(locationAvailability, clientManager); } }; @@ -106,8 +87,10 @@ public void onLocationChanged(final Location location) throws RemoteException { isBound = false; } - public FusedLocationProviderApiImpl(FusedLocationServiceConnectionManager connectionManager) { + public FusedLocationProviderApiImpl(FusedLocationServiceConnectionManager connectionManager, + FusedLocationServiceCallbackManager callbackManager) { serviceConnectionManager = connectionManager; + serviceCallbackManager = callbackManager; serviceConnectionManager.setEventCallbacks(this); } diff --git a/lost/src/main/java/com/mapzen/android/lost/internal/FusedLocationServiceCallbackManager.java b/lost/src/main/java/com/mapzen/android/lost/internal/FusedLocationServiceCallbackManager.java new file mode 100644 index 0000000..9111e07 --- /dev/null +++ b/lost/src/main/java/com/mapzen/android/lost/internal/FusedLocationServiceCallbackManager.java @@ -0,0 +1,68 @@ +package com.mapzen.android.lost.internal; + +import com.mapzen.android.lost.api.LocationAvailability; +import com.mapzen.android.lost.api.LocationResult; + +import android.content.Context; +import android.location.Location; +import android.os.RemoteException; + +import java.util.ArrayList; + +/** + * Handles callbacks received in {@link FusedLocationProviderApiImpl} from + * {@link FusedLocationProviderService}. + */ +public class FusedLocationServiceCallbackManager { + + /** + * Called when a new location has been received. This method handles dispatching changes to all + * {@link com.mapzen.android.lost.api.LocationListener}s, {@link android.app.PendingIntent}s, and + * {@link com.mapzen.android.lost.api.LocationCallback}s which are registered. If the + * {@link IFusedLocationProviderService} is null, an {@link IllegalStateException} will be thrown. + * @param context + * @param location + * @param clientManager + * @param service + */ + void onLocationChanged(Context context, Location location, LostClientManager clientManager, + IFusedLocationProviderService service) { + if (service == null) { + throw new IllegalStateException("Location update received after client was " + + "disconnected. Did you forget to unregister location updates before " + + "disconnecting?"); + } + + ReportedChanges changes = clientManager.reportLocationChanged(location); + + LocationAvailability availability; + try { + availability = service.getLocationAvailability(); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + + ArrayList locations = new ArrayList<>(); + locations.add(location); + final LocationResult result = LocationResult.create(locations); + ReportedChanges pendingIntentChanges = clientManager.sendPendingIntent( + context, location, availability, result); + + ReportedChanges callbackChanges = clientManager.reportLocationResult(location, result); + + changes.putAll(pendingIntentChanges); + changes.putAll(callbackChanges); + clientManager.updateReportedValues(changes); + } + + /** + * Handles notifying all registered {@link LocationCallback}s that {@link LocationAvailability} + * has changed. + * @param locationAvailability + * @param clientManager + */ + void onLocationAvailabilityChanged(LocationAvailability locationAvailability, + LostClientManager clientManager) { + clientManager.notifyLocationAvailability(locationAvailability); + } +} diff --git a/lost/src/test/java/com/mapzen/android/lost/internal/FusedLocationProviderApiImplTest.java b/lost/src/test/java/com/mapzen/android/lost/internal/FusedLocationProviderApiImplTest.java index 2f5e38b..580b332 100644 --- a/lost/src/test/java/com/mapzen/android/lost/internal/FusedLocationProviderApiImplTest.java +++ b/lost/src/test/java/com/mapzen/android/lost/internal/FusedLocationProviderApiImplTest.java @@ -61,7 +61,8 @@ public class FusedLocationProviderApiImplTest extends BaseRobolectricTest { FusedLocationServiceConnectionManager.EventCallbacks.class)); Mockito.doCallRealMethod().when(connectionManager).connect(any(Context.class), any( LostApiClient.ConnectionCallbacks.class)); - api = new FusedLocationProviderApiImpl(connectionManager); + api = new FusedLocationProviderApiImpl(connectionManager, + new FusedLocationServiceCallbackManager()); api.connect(application, null); api.service = service; } diff --git a/lost/src/test/java/com/mapzen/android/lost/internal/FusedLocationServiceCallbackManagerTest.java b/lost/src/test/java/com/mapzen/android/lost/internal/FusedLocationServiceCallbackManagerTest.java new file mode 100644 index 0000000..231f8dc --- /dev/null +++ b/lost/src/test/java/com/mapzen/android/lost/internal/FusedLocationServiceCallbackManagerTest.java @@ -0,0 +1,91 @@ +package com.mapzen.android.lost.internal; + +import com.mapzen.android.lost.api.LocationAvailability; +import com.mapzen.android.lost.api.LocationResult; + +import org.junit.Test; + +import android.content.Context; +import android.location.Location; +import android.os.RemoteException; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class FusedLocationServiceCallbackManagerTest { + + FusedLocationServiceCallbackManager callbackManager = + new FusedLocationServiceCallbackManager(); + + @Test(expected = IllegalStateException.class) + public void onLocationChanged_shouldThrowIfServiceDisconnected() { + callbackManager.onLocationChanged(mock(Context.class), mock(Location.class), + mock(LostClientManager.class), null); + } + + @Test public void onLocationChanged_shouldReportLocationChanged() { + LostClientManager clientManager = mock(LostClientManager.class); + Location location = mock(Location.class); + when(clientManager.reportLocationChanged(any(Location.class))). + thenReturn(mock(ReportedChanges.class)); + callbackManager.onLocationChanged(mock(Context.class), location, clientManager, + mock(IFusedLocationProviderService.class)); + verify(clientManager).reportLocationChanged(location); + } + + @Test public void onLocationChanged_shouldSendPendingIntent() { + LostClientManager clientManager = mock(LostClientManager.class); + when(clientManager.reportLocationChanged(any(Location.class))). + thenReturn(mock(ReportedChanges.class)); + IFusedLocationProviderService service = mock(IFusedLocationProviderService.class); + LocationAvailability locationAvailability = mock(LocationAvailability.class); + try { + when(service.getLocationAvailability()).thenReturn(locationAvailability); + } catch (RemoteException e) { + e.printStackTrace(); + } + Context context = mock(Context.class); + Location location = mock(Location.class); + callbackManager.onLocationChanged(context, location, clientManager, service); + verify(clientManager).sendPendingIntent(eq(context), eq(location), eq(locationAvailability), + any(LocationResult.class)); + } + + @Test public void onLocationChanged_shouldReportLocationResult() { + LostClientManager clientManager = mock(LostClientManager.class); + when(clientManager.reportLocationChanged(any(Location.class))). + thenReturn(mock(ReportedChanges.class)); + IFusedLocationProviderService service = mock(IFusedLocationProviderService.class); + Location location = mock(Location.class); + callbackManager.onLocationChanged(mock(Context.class), location, clientManager, service); + verify(clientManager).reportLocationResult(eq(location), any(LocationResult.class)); + } + + @Test public void onLocationChanged_shouldUpdateReportedValues() { + LostClientManager clientManager = mock(LostClientManager.class); + ReportedChanges changes = mock(ReportedChanges.class); + when(clientManager.reportLocationChanged(any(Location.class))).thenReturn(changes); + ReportedChanges pendingIntentChanges = mock(ReportedChanges.class); + when(clientManager.sendPendingIntent(any(Context.class), any(Location.class), + any(LocationAvailability.class), any(LocationResult.class))).thenReturn( + pendingIntentChanges); + ReportedChanges callbackChanges = mock(ReportedChanges.class); + when(clientManager.reportLocationResult(any(Location.class), any(LocationResult.class))). + thenReturn(callbackChanges); + callbackManager.onLocationChanged(mock(Context.class), mock(Location.class), clientManager, + mock(IFusedLocationProviderService.class)); + verify(changes).putAll(pendingIntentChanges); + verify(changes).putAll(callbackChanges); + verify(clientManager).updateReportedValues(changes); + } + + @Test public void onLocationAvailabilityChanged_shouldNotifyLocationAvailability() { + LostClientManager clientManager = mock(LostClientManager.class); + LocationAvailability locationAvailability = mock(LocationAvailability.class); + callbackManager.onLocationAvailabilityChanged(locationAvailability, clientManager); + verify(clientManager).notifyLocationAvailability(locationAvailability); + } +}