package cgeo.geocaching.playservices;
import cgeo.geocaching.sensors.GeoData;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.utils.AndroidRxUtils;
import cgeo.geocaching.utils.Log;
import android.content.Context;
import android.location.Location;
import android.os.Bundle;
import android.support.annotation.NonNull;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
import io.reactivex.disposables.Disposable;
import io.reactivex.disposables.Disposables;
import io.reactivex.functions.Predicate;
import io.reactivex.observers.DisposableObserver;
import io.reactivex.subjects.ReplaySubject;
public class LocationProvider extends LocationCallback implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
private static final LocationRequest LOCATION_REQUEST =
LocationRequest.create().setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY).setInterval(2000).setFastestInterval(250);
private static final LocationRequest LOCATION_REQUEST_LOW_POWER =
LocationRequest.create().setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY).setInterval(10000).setFastestInterval(5000);
private static final AtomicInteger mostPreciseCount = new AtomicInteger(0);
private static final AtomicInteger lowPowerCount = new AtomicInteger(0);
private static LocationProvider instance = null;
private static final ReplaySubject<GeoData> subject = ReplaySubject.createWithSize(1);
private final GoogleApiClient locationClient;
private static synchronized LocationProvider getInstance(final Context context) {
if (instance == null) {
instance = new LocationProvider(context);
}
return instance;
}
private synchronized void updateRequest() {
if (locationClient.isConnected()) {
try {
if (mostPreciseCount.get() > 0) {
Log.d("LocationProvider: requesting most precise locations");
LocationServices.FusedLocationApi.requestLocationUpdates(locationClient, LOCATION_REQUEST, this, AndroidRxUtils.looperCallbacksLooper);
} else if (lowPowerCount.get() > 0) {
Log.d("LocationProvider: requesting low-power locations");
LocationServices.FusedLocationApi.requestLocationUpdates(locationClient, LOCATION_REQUEST_LOW_POWER, this, AndroidRxUtils.looperCallbacksLooper);
} else {
Log.d("LocationProvider: stopping location requests");
LocationServices.FusedLocationApi.removeLocationUpdates(locationClient, this);
}
} catch (final SecurityException e) {
Log.w("Security exception when accessing fused location services", e);
}
}
}
private static Observable<GeoData> get(final Context context, final AtomicInteger reference) {
final LocationProvider instance = getInstance(context);
return Observable.create(new ObservableOnSubscribe<GeoData>() {
@Override
public void subscribe(final ObservableEmitter<GeoData> emitter) throws Exception {
if (reference.incrementAndGet() == 1) {
instance.updateRequest();
}
final Disposable disposable = subject.subscribeWith(new DisposableObserver<GeoData>() {
@Override
public void onNext(final GeoData value) {
emitter.onNext(value);
}
@Override
public void onError(final Throwable e) {
emitter.onError(e);
}
@Override
public void onComplete() {
emitter.onComplete();
}
});
emitter.setDisposable(Disposables.fromRunnable(new Runnable() {
@Override
public void run() {
disposable.dispose();
AndroidRxUtils.looperCallbacksScheduler.scheduleDirect(new Runnable() {
@Override
public void run() {
if (reference.decrementAndGet() == 0) {
instance.updateRequest();
}
}
}, 2500, TimeUnit.MILLISECONDS);
}
}));
}
});
}
public static Observable<GeoData> getMostPrecise(final Context context) {
return get(context, mostPreciseCount);
}
public static Observable<GeoData> getLowPower(final Context context) {
// Low-power location without the last stored location
final Observable<GeoData> lowPowerObservable = get(context, lowPowerCount).skip(1);
// High-power location without the last stored location
final Observable<GeoData> highPowerObservable = get(context, mostPreciseCount).skip(1);
// Use either low-power (with a 6 seconds head start) or high-power observables to obtain a location
// no less precise than 20 meters.
final Observable<GeoData> untilPreciseEnoughObservable =
lowPowerObservable.mergeWith(highPowerObservable.delaySubscription(6, TimeUnit.SECONDS))
.takeUntil(new Predicate<GeoData>() {
@Override
public boolean test(final GeoData geoData) {
return geoData.getAccuracy() <= 20;
}
});
// After sending the last known location, try to get a precise location then use the low-power mode. If no
// location information is given for 25 seconds (if the network location is turned off for example), get
// back to the precise location and try again.
return subject.take(1).concatWith(untilPreciseEnoughObservable.concatWith(lowPowerObservable).timeout(25, TimeUnit.SECONDS).retry());
}
/**
* Build a new geo data provider object.
* <p/>
* There is no need to instantiate more than one such object in an application, as observers can be added
* at will.
*
* @param context the context used to retrieve the system services
*/
private LocationProvider(final Context context) {
final GeoData initialLocation = GeoData.getInitialLocation(context);
subject.onNext(initialLocation != null ? initialLocation : GeoData.DUMMY_LOCATION);
locationClient = new GoogleApiClient.Builder(context)
.addApi(LocationServices.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
locationClient.connect();
}
@Override
public void onConnected(final Bundle bundle) {
updateRequest();
}
@Override
public void onConnectionFailed(@NonNull final ConnectionResult connectionResult) {
Log.e("cannot connect to Google Play location service: " + connectionResult);
subject.onError(new RuntimeException("Connection failed: " + connectionResult));
}
@Override
public void onLocationResult(final LocationResult result) {
final Location location = result.getLastLocation();
if (Settings.useLowPowerMode()) {
location.setProvider(GeoData.LOW_POWER_PROVIDER);
}
subject.onNext(new GeoData(location));
}
@Override
public void onConnectionSuspended(final int arg0) {
// empty
}
}