aboutsummaryrefslogtreecommitdiffstats
path: root/main/src/cgeo/geocaching/sensors/GeoDataProvider.java
diff options
context:
space:
mode:
Diffstat (limited to 'main/src/cgeo/geocaching/sensors/GeoDataProvider.java')
-rw-r--r--main/src/cgeo/geocaching/sensors/GeoDataProvider.java263
1 files changed, 263 insertions, 0 deletions
diff --git a/main/src/cgeo/geocaching/sensors/GeoDataProvider.java b/main/src/cgeo/geocaching/sensors/GeoDataProvider.java
new file mode 100644
index 0000000..05d9467
--- /dev/null
+++ b/main/src/cgeo/geocaching/sensors/GeoDataProvider.java
@@ -0,0 +1,263 @@
+package cgeo.geocaching.sensors;
+
+import cgeo.geocaching.utils.Log;
+
+import org.apache.commons.lang3.StringUtils;
+import rx.Observable;
+import rx.Observable.OnSubscribe;
+import rx.Scheduler.Inner;
+import rx.Subscriber;
+import rx.Subscription;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.functions.Action0;
+import rx.functions.Action1;
+import rx.observables.ConnectableObservable;
+import rx.subjects.BehaviorSubject;
+import rx.subscriptions.CompositeSubscription;
+import rx.subscriptions.Subscriptions;
+
+import android.content.Context;
+import android.location.GpsSatellite;
+import android.location.GpsStatus;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
+
+import java.util.concurrent.TimeUnit;
+
+public class GeoDataProvider implements OnSubscribe<IGeoData> {
+
+ private static final String LAST_LOCATION_PSEUDO_PROVIDER = "last";
+ private final LocationManager geoManager;
+ private final LocationData gpsLocation = new LocationData();
+ private final LocationData netLocation = new LocationData();
+ private final BehaviorSubject<IGeoData> subject;
+
+ public boolean gpsEnabled = false;
+ public int satellitesVisible = 0;
+ public int satellitesFixed = 0;
+
+ private static class LocationData {
+ public Location location;
+ public long timestamp = 0;
+
+ public void update(final Location location) {
+ this.location = location;
+ timestamp = System.currentTimeMillis();
+ }
+
+ public boolean isRecent() {
+ return isValid() && System.currentTimeMillis() < timestamp + 30000;
+ }
+
+ public boolean isValid() {
+ return location != null;
+ }
+ }
+
+ /**
+ * 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
+ */
+ protected GeoDataProvider(final Context context) {
+ geoManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
+ subject = BehaviorSubject.create(findInitialLocation());
+ }
+
+ public static Observable<IGeoData> create(final Context context) {
+ final GeoDataProvider provider = new GeoDataProvider(context);
+ return provider.worker.refCount();
+ }
+
+ @Override
+ public void call(final Subscriber<? super IGeoData> subscriber) {
+ subject.subscribe(subscriber);
+ }
+
+ final ConnectableObservable<IGeoData> worker = new ConnectableObservable<IGeoData>(this) {
+ @Override
+ public Subscription connect() {
+ final CompositeSubscription subscription = new CompositeSubscription();
+ AndroidSchedulers.mainThread().schedule(new Action1<Inner>() {
+ @Override
+ public void call(final Inner inner) {
+ final GpsStatus.Listener gpsStatusListener = new GpsStatusListener();
+ geoManager.addGpsStatusListener(gpsStatusListener);
+
+ final Listener networkListener = new Listener(LocationManager.NETWORK_PROVIDER, netLocation);
+ final Listener gpsListener = new Listener(LocationManager.GPS_PROVIDER, gpsLocation);
+
+ for (final Listener listener : new Listener[] { networkListener, gpsListener }) {
+ try {
+ geoManager.requestLocationUpdates(listener.locationProvider, 0, 0, listener);
+ } catch (final Exception e) {
+ Log.w("There is no location provider " + listener.locationProvider);
+ }
+ }
+
+ subscription.add(Subscriptions.create(new Action0() {
+ @Override
+ public void call() {
+ AndroidSchedulers.mainThread().schedule(new Action1<Inner>() {
+ @Override
+ public void call(final Inner inner) {
+ geoManager.removeUpdates(networkListener);
+ geoManager.removeUpdates(gpsListener);
+ geoManager.removeGpsStatusListener(gpsStatusListener);
+ }
+ }, 2500, TimeUnit.MILLISECONDS);
+ }
+ }));
+ }
+ });
+ return subscription;
+ }
+ };
+
+ private IGeoData findInitialLocation() {
+ final Location initialLocation = new Location(LAST_LOCATION_PSEUDO_PROVIDER);
+ try {
+ // Try to find a sensible initial location from the last locations known to Android.
+ final Location lastGpsLocation = geoManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
+ final Location lastNetworkLocation = geoManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
+
+ // If both providers are non-null, take the most recent one
+ if (lastGpsLocation != null && lastNetworkLocation != null) {
+ if (lastGpsLocation.getTime() >= lastNetworkLocation.getTime()) {
+ copyCoords(initialLocation, lastGpsLocation);
+ } else {
+ copyCoords(initialLocation, lastNetworkLocation);
+ }
+ } else if (lastGpsLocation != null) {
+ copyCoords(initialLocation, lastGpsLocation);
+ } else if (lastNetworkLocation != null) {
+ copyCoords(initialLocation, lastNetworkLocation);
+ } else {
+ Log.i("GeoDataProvider: no last known location available");
+ }
+ } catch (final Exception e) {
+ // This error is non-fatal as its only consequence is that we will start with a dummy location
+ // instead of a previously known one.
+ Log.e("GeoDataProvider: error when retrieving last known location", e);
+ }
+ // Start with an historical GeoData just in case someone queries it before we get
+ // a chance to get any information.
+ return new GeoData(initialLocation, false, 0, 0, true);
+ }
+
+ private static void copyCoords(final Location target, final Location source) {
+ target.setLatitude(source.getLatitude());
+ target.setLongitude(source.getLongitude());
+ }
+
+ private class Listener implements LocationListener {
+ private final String locationProvider;
+ private final LocationData locationData;
+
+ Listener(final String locationProvider, final LocationData locationData) {
+ this.locationProvider = locationProvider;
+ this.locationData = locationData;
+ }
+
+ @Override
+ public void onStatusChanged(final String provider, final int status, final Bundle extras) {
+ // nothing
+ }
+
+ @Override
+ public void onProviderDisabled(final String provider) {
+ // nothing
+ }
+
+ @Override
+ public void onProviderEnabled(final String provider) {
+ // nothing
+ }
+
+ @Override
+ public void onLocationChanged(final Location location) {
+ locationData.update(location);
+ selectBest();
+ }
+ }
+
+ private final class GpsStatusListener implements GpsStatus.Listener {
+
+ @Override
+ public void onGpsStatusChanged(final int event) {
+ boolean changed = false;
+ switch (event) {
+ case GpsStatus.GPS_EVENT_FIRST_FIX:
+ case GpsStatus.GPS_EVENT_SATELLITE_STATUS: {
+ final GpsStatus status = geoManager.getGpsStatus(null);
+ int visible = 0;
+ int fixed = 0;
+ for (final GpsSatellite satellite : status.getSatellites()) {
+ if (satellite.usedInFix()) {
+ fixed++;
+ }
+ visible++;
+ }
+ if (visible != satellitesVisible || fixed != satellitesFixed) {
+ satellitesVisible = visible;
+ satellitesFixed = fixed;
+ changed = true;
+ }
+ break;
+ }
+ case GpsStatus.GPS_EVENT_STARTED:
+ if (!gpsEnabled) {
+ gpsEnabled = true;
+ changed = true;
+ }
+ break;
+ case GpsStatus.GPS_EVENT_STOPPED:
+ if (gpsEnabled) {
+ gpsEnabled = false;
+ satellitesFixed = 0;
+ satellitesVisible = 0;
+ changed = true;
+ }
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+
+ if (changed) {
+ selectBest();
+ }
+ }
+ }
+
+ private LocationData best() {
+ if (gpsLocation.isRecent() || !netLocation.isValid()) {
+ return gpsLocation.isValid() ? gpsLocation : null;
+ }
+ if (!gpsLocation.isValid()) {
+ return netLocation;
+ }
+ return gpsLocation.timestamp > netLocation.timestamp ? gpsLocation : netLocation;
+ }
+
+ private void selectBest() {
+ assign(best());
+ }
+
+ private void assign(final LocationData locationData) {
+ if (locationData == null) {
+ return;
+ }
+
+ // We do not necessarily get signalled when satellites go to 0/0.
+ final int visible = gpsLocation.isRecent() ? satellitesVisible : 0;
+ final boolean pseudoLocation = StringUtils.equals(locationData.location.getProvider(), LAST_LOCATION_PSEUDO_PROVIDER);
+ final IGeoData current = new GeoData(locationData.location, gpsEnabled, visible, satellitesFixed, pseudoLocation);
+ subject.onNext(current);
+ }
+
+}