package pl.charmas.android.reactivelocation; import android.app.PendingIntent; import android.content.Context; import android.location.Address; import android.location.Location; import android.support.annotation.Nullable; import android.support.annotation.RequiresPermission; import com.google.android.gms.common.api.Api; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.Result; import com.google.android.gms.common.api.Status; import com.google.android.gms.location.ActivityRecognitionResult; import com.google.android.gms.location.GeofencingRequest; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; import com.google.android.gms.location.LocationSettingsRequest; import com.google.android.gms.location.LocationSettingsResult; import com.google.android.gms.location.places.AutocompleteFilter; import com.google.android.gms.location.places.AutocompletePredictionBuffer; import com.google.android.gms.location.places.PlaceBuffer; import com.google.android.gms.location.places.PlaceFilter; import com.google.android.gms.location.places.PlaceLikelihoodBuffer; import com.google.android.gms.location.places.PlacePhotoMetadata; import com.google.android.gms.location.places.PlacePhotoMetadataResult; import com.google.android.gms.location.places.PlacePhotoResult; import com.google.android.gms.location.places.Places; import com.google.android.gms.maps.model.LatLngBounds; import java.util.List; import java.util.Locale; import pl.charmas.android.reactivelocation.observables.GoogleAPIClientObservable; import pl.charmas.android.reactivelocation.observables.PendingResultObservable; import pl.charmas.android.reactivelocation.observables.activity.ActivityUpdatesObservable; import pl.charmas.android.reactivelocation.observables.geocode.GeocodeObservable; import pl.charmas.android.reactivelocation.observables.geocode.ReverseGeocodeObservable; import pl.charmas.android.reactivelocation.observables.geofence.AddGeofenceObservable; import pl.charmas.android.reactivelocation.observables.geofence.RemoveGeofenceObservable; import pl.charmas.android.reactivelocation.observables.location.AddLocationIntentUpdatesObservable; import pl.charmas.android.reactivelocation.observables.location.LastKnownLocationObservable; import pl.charmas.android.reactivelocation.observables.location.LocationUpdatesObservable; import pl.charmas.android.reactivelocation.observables.location.MockLocationObservable; import pl.charmas.android.reactivelocation.observables.location.RemoveLocationIntentUpdatesObservable; import rx.Observable; import rx.functions.Func1; /** * Factory of observables that can manipulate location * delivered by Google Play Services. */ public class ReactiveLocationProvider { private final Context ctx; public ReactiveLocationProvider(Context ctx) { this.ctx = ctx; } /** * Creates observable that obtains last known location and than completes. * Delivered location is never null - when it is unavailable Observable completes without emitting * any value. * <p/> * Observable can report {@link pl.charmas.android.reactivelocation.observables.GoogleAPIConnectionException} * when there are trouble connecting with Google Play Services and other exceptions that * can be thrown on {@link com.google.android.gms.location.FusedLocationProviderApi#getLastLocation(com.google.android.gms.common.api.GoogleApiClient)}. * Everything is delivered by {@link rx.Observer#onError(Throwable)}. * * @return observable that serves last know location */ @RequiresPermission( anyOf = {"android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION"} ) public Observable<Location> getLastKnownLocation() { return LastKnownLocationObservable.createObservable(ctx); } /** * Creates observable that allows to observe infinite stream of location updates. * To stop the stream you have to unsubscribe from observable - location updates are * then disconnected. * <p/> * Observable can report {@link pl.charmas.android.reactivelocation.observables.GoogleAPIConnectionException} * when there are trouble connecting with Google Play Services and other exceptions that * can be thrown on {@link com.google.android.gms.location.FusedLocationProviderApi#requestLocationUpdates(com.google.android.gms.common.api.GoogleApiClient, com.google.android.gms.location.LocationRequest, com.google.android.gms.location.LocationListener)}. * Everything is delivered by {@link rx.Observer#onError(Throwable)}. * * @param locationRequest request object with info about what kind of location you need * @return observable that serves infinite stream of location updates */ @RequiresPermission( anyOf = {"android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION"} ) public Observable<Location> getUpdatedLocation(LocationRequest locationRequest) { return LocationUpdatesObservable.createObservable(ctx, locationRequest); } /** * Returns an observable which activates mock location mode when subscribed to, using the * supplied observable as a source of mock locations. Mock locations will replace normal * location information for all users of the FusedLocationProvider API on the device while this * observable is subscribed to. * <p/> * To use this method, mock locations must be enabled in developer options and your application * must hold the android.permission.ACCESS_MOCK_LOCATION permission, or a {@link java.lang.SecurityException} * will be thrown. * <p/> * All statuses that are not successful will be reported as {@link pl.charmas.android.reactivelocation.observables.StatusException}. * <p/> * Every exception is delivered by {@link rx.Observer#onError(Throwable)}. * * @param sourceLocationObservable observable that emits {@link android.location.Location} instances suitable to use as mock locations * @return observable that emits {@link com.google.android.gms.common.api.Status} */ @RequiresPermission( allOf = {"android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_MOCK_LOCATION"} ) public Observable<Status> mockLocation(Observable<Location> sourceLocationObservable) { return MockLocationObservable.createObservable(ctx, sourceLocationObservable); } /** * Creates an observable that adds a {@link android.app.PendingIntent} as a location listener. * <p/> * This invokes {@link com.google.android.gms.location.FusedLocationProviderApi#requestLocationUpdates(com.google.android.gms.common.api.GoogleApiClient, com.google.android.gms.location.LocationRequest, android.app.PendingIntent)}. * <p/> * When location updates are no longer required, a call to {@link #removeLocationUpdates(android.app.PendingIntent)} * should be made. * <p/> * In case of unsuccessful status {@link pl.charmas.android.reactivelocation.observables.StatusException} is delivered. * * @param locationRequest request object with info about what kind of location you need * @param intent PendingIntent that will be called with location updates * @return observable that adds the request and PendingIntent */ @RequiresPermission( anyOf = {"android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION"} ) public Observable<Status> requestLocationUpdates(LocationRequest locationRequest, PendingIntent intent) { return AddLocationIntentUpdatesObservable.createObservable(ctx, locationRequest, intent); } /** * Observable that can be used to remove {@link android.app.PendingIntent} location updates. * <p/> * In case of unsuccessful status {@link pl.charmas.android.reactivelocation.observables.StatusException} is delivered. * * @param intent PendingIntent to remove location updates for * @return observable that removes the PendingIntent */ public Observable<Status> removeLocationUpdates(PendingIntent intent) { return RemoveLocationIntentUpdatesObservable.createObservable(ctx, intent); } /** * Creates observable that translates latitude and longitude to list of possible addresses using * included Geocoder class. In case geocoder fails with IOException("Service not Available") fallback * decoder is used using google web api. You should subscribe for this observable on I/O thread. * The stream finishes after address list is available. * * @param lat latitude * @param lng longitude * @param maxResults maximal number of results you are interested in * @return observable that serves list of address based on location */ public Observable<List<Address>> getReverseGeocodeObservable(double lat, double lng, int maxResults) { return ReverseGeocodeObservable.createObservable(ctx, Locale.getDefault(), lat, lng, maxResults); } /** * Creates observable that translates latitude and longitude to list of possible addresses using * included Geocoder class. In case geocoder fails with IOException("Service not Available") fallback * decoder is used using google web api. You should subscribe for this observable on I/O thread. * The stream finishes after address list is available. * * @param locale locale for address language * @param lat latitude * @param lng longitude * @param maxResults maximal number of results you are interested in * @return observable that serves list of address based on location */ public Observable<List<Address>> getReverseGeocodeObservable(Locale locale, double lat, double lng, int maxResults) { return ReverseGeocodeObservable.createObservable(ctx, locale, lat, lng, maxResults); } /** * Creates observable that translates a street address or other description into a list of * possible addresses using included Geocoder class. You should subscribe for this * observable on I/O thread. * The stream finishes after address list is available. * * @param locationName a user-supplied description of a location * @param maxResults max number of results you are interested in * @return observable that serves list of address based on location name */ public Observable<List<Address>> getGeocodeObservable(String locationName, int maxResults) { return getGeocodeObservable(locationName, maxResults, null); } /** * Creates observable that translates a street address or other description into a list of * possible addresses using included Geocoder class. You should subscribe for this * observable on I/O thread. * The stream finishes after address list is available. * <p/> * You may specify a bounding box for the search results. * * @param locationName a user-supplied description of a location * @param maxResults max number of results you are interested in * @param bounds restricts the results to geographical bounds. May be null * @return observable that serves list of address based on location name */ public Observable<List<Address>> getGeocodeObservable(String locationName, int maxResults, LatLngBounds bounds) { return GeocodeObservable.createObservable(ctx, locationName, maxResults, bounds); } /** * Creates observable that adds request and completes when the action is done. * <p/> * Observable can report {@link pl.charmas.android.reactivelocation.observables.GoogleAPIConnectionException} * when there are trouble connecting with Google Play Services. * <p/> * In case of unsuccessful status {@link pl.charmas.android.reactivelocation.observables.StatusException} is delivered. * <p/> * Other exceptions will be reported that can be thrown on {@link com.google.android.gms.location.GeofencingApi#addGeofences(com.google.android.gms.common.api.GoogleApiClient, com.google.android.gms.location.GeofencingRequest, android.app.PendingIntent)} * * @param geofenceTransitionPendingIntent pending intent to register on geofence transition * @param request list of request to add * @return observable that adds request */ @RequiresPermission("android.permission.ACCESS_FINE_LOCATION") public Observable<Status> addGeofences(PendingIntent geofenceTransitionPendingIntent, GeofencingRequest request) { return AddGeofenceObservable.createObservable(ctx, request, geofenceTransitionPendingIntent); } /** * Observable that can be used to remove geofences from LocationClient. * <p/> * In case of unsuccessful status {@link pl.charmas.android.reactivelocation.observables.StatusException} is delivered. * <p/> * Other exceptions will be reported that can be thrown on {@link com.google.android.gms.location.GeofencingApi#removeGeofences(com.google.android.gms.common.api.GoogleApiClient, android.app.PendingIntent)}. * <p/> * Every exception is delivered by {@link rx.Observer#onError(Throwable)}. * * @param pendingIntent key of registered geofences * @return observable that removed geofences */ public Observable<Status> removeGeofences(PendingIntent pendingIntent) { return RemoveGeofenceObservable.createObservable(ctx, pendingIntent); } /** * Observable that can be used to remove geofences from LocationClient. * <p/> * In case of unsuccessful status {@link pl.charmas.android.reactivelocation.observables.StatusException} is delivered. * <p/> * Other exceptions will be reported that can be thrown on {@link com.google.android.gms.location.GeofencingApi#removeGeofences(com.google.android.gms.common.api.GoogleApiClient, java.util.List)}. * <p/> * Every exception is delivered by {@link rx.Observer#onError(Throwable)}. * * @param requestIds geofences to remove * @return observable that removed geofences */ public Observable<Status> removeGeofences(List<String> requestIds) { return RemoveGeofenceObservable.createObservable(ctx, requestIds); } /** * Observable that can be used to observe activity provided by Actity Recognition mechanism. * * @param detectIntervalMiliseconds detecion interval * @return observable that provides activity recognition */ public Observable<ActivityRecognitionResult> getDetectedActivity(int detectIntervalMiliseconds) { return ActivityUpdatesObservable.createObservable(ctx, detectIntervalMiliseconds); } /** * Observable that can be used to check settings state for given location request. * * @param locationRequest location request * @return observable that emits check result of location settings * @see com.google.android.gms.location.SettingsApi */ public Observable<LocationSettingsResult> checkLocationSettings(final LocationSettingsRequest locationRequest) { return getGoogleApiClientObservable(LocationServices.API) .flatMap(new Func1<GoogleApiClient, Observable<LocationSettingsResult>>() { @Override public Observable<LocationSettingsResult> call(GoogleApiClient googleApiClient) { return fromPendingResult(LocationServices.SettingsApi.checkLocationSettings(googleApiClient, locationRequest)); } }); } /** * Returns observable that fetches current place from Places API. To flatmap and auto release * buffer to {@link com.google.android.gms.location.places.PlaceLikelihood} observable use * {@link DataBufferObservable}. * * @param placeFilter filter * @return observable that emits current places buffer and completes */ public Observable<PlaceLikelihoodBuffer> getCurrentPlace(@Nullable final PlaceFilter placeFilter) { return getGoogleApiClientObservable(Places.PLACE_DETECTION_API, Places.GEO_DATA_API) .flatMap(new Func1<GoogleApiClient, Observable<PlaceLikelihoodBuffer>>() { @Override public Observable<PlaceLikelihoodBuffer> call(GoogleApiClient api) { return fromPendingResult(Places.PlaceDetectionApi.getCurrentPlace(api, placeFilter)); } }); } /** * Returns observable that fetches a place from the Places API using the place ID. * * @param placeId id for place * @return observable that emits places buffer and completes */ public Observable<PlaceBuffer> getPlaceById(@Nullable final String placeId) { return getGoogleApiClientObservable(Places.PLACE_DETECTION_API, Places.GEO_DATA_API) .flatMap(new Func1<GoogleApiClient, Observable<PlaceBuffer>>() { @Override public Observable<PlaceBuffer> call(GoogleApiClient api) { return fromPendingResult(Places.GeoDataApi.getPlaceById(api, placeId)); } }); } /** * Returns observable that fetches autocomplete predictions from Places API. To flatmap and autorelease * {@link com.google.android.gms.location.places.AutocompletePredictionBuffer} you can use * {@link DataBufferObservable}. * * @param query search query * @param bounds bounds where to fetch suggestions from * @param filter filter * @return observable with suggestions buffer and completes */ public Observable<AutocompletePredictionBuffer> getPlaceAutocompletePredictions(final String query, final LatLngBounds bounds, final AutocompleteFilter filter) { return getGoogleApiClientObservable(Places.PLACE_DETECTION_API, Places.GEO_DATA_API) .flatMap(new Func1<GoogleApiClient, Observable<AutocompletePredictionBuffer>>() { @Override public Observable<AutocompletePredictionBuffer> call(GoogleApiClient api) { return fromPendingResult(Places.GeoDataApi.getAutocompletePredictions(api, query, bounds, filter)); } }); } /** * Returns observable that fetches photo metadata from the Places API using the place ID. * * @param placeId id for place * @return observable that emits metadata buffer and completes */ public Observable<PlacePhotoMetadataResult> getPhotoMetadataById(final String placeId) { return getGoogleApiClientObservable(Places.PLACE_DETECTION_API, Places.GEO_DATA_API) .flatMap(new Func1<GoogleApiClient, Observable<PlacePhotoMetadataResult>>() { @Override public Observable<PlacePhotoMetadataResult> call(GoogleApiClient api) { return fromPendingResult(Places.GeoDataApi.getPlacePhotos(api, placeId)); } }); } /** * Returns observable that fetches a placePhotoMetadata from the Places API using the place placePhotoMetadata metadata. * Use after fetching the place placePhotoMetadata metadata with {@link ReactiveLocationProvider#getPhotoMetadataById(String)} * * @param placePhotoMetadata the place photo meta data * @return observable that emits the photo result and completes */ public Observable<PlacePhotoResult> getPhotoForMetadata(final PlacePhotoMetadata placePhotoMetadata) { return getGoogleApiClientObservable(Places.PLACE_DETECTION_API, Places.GEO_DATA_API) .flatMap(new Func1<GoogleApiClient, Observable<PlacePhotoResult>>() { @Override public Observable<PlacePhotoResult> call(GoogleApiClient api) { return fromPendingResult(placePhotoMetadata.getPhoto(api)); } }); } /** * Observable that emits {@link com.google.android.gms.common.api.GoogleApiClient} object after connection. * In case of error {@link pl.charmas.android.reactivelocation.observables.GoogleAPIConnectionException} is emmited. * When connection to Google Play Services is suspended {@link pl.charmas.android.reactivelocation.observables.GoogleAPIConnectionSuspendedException} * is emitted as error. * Do not disconnect from apis client manually - just unsubscribe. * * @param apis collection of apis to connect to * @return observable that emits apis client after successful connection */ public Observable<GoogleApiClient> getGoogleApiClientObservable(Api... apis) { //noinspection unchecked return GoogleAPIClientObservable.create(ctx, apis); } /** * Util method that wraps {@link com.google.android.gms.common.api.PendingResult} in Observable. * * @param result pending result to wrap * @param <T> parameter type of result * @return observable that emits pending result and completes */ public static <T extends Result> Observable<T> fromPendingResult(PendingResult<T> result) { return Observable.create(new PendingResultObservable<>(result)); } }