aboutsummaryrefslogtreecommitdiffstats
path: root/main/src/cgeo/geocaching/sensors
diff options
context:
space:
mode:
authorSamuel Tardieu <sam@rfc1149.net>2014-03-06 08:46:44 +0100
committerSamuel Tardieu <sam@rfc1149.net>2014-03-06 18:31:58 +0100
commit48b7cbaa0b469794088d41038192366fea802190 (patch)
tree8743ff2a6589957c01aabe4af3434f9d7fe7be7c /main/src/cgeo/geocaching/sensors
parentbc29353af3aaa64469968a2055690bdafd299309 (diff)
downloadcgeo-48b7cbaa0b469794088d41038192366fea802190.zip
cgeo-48b7cbaa0b469794088d41038192366fea802190.tar.gz
cgeo-48b7cbaa0b469794088d41038192366fea802190.tar.bz2
refactoring: create cgeo.geocaching.sensors package
Diffstat (limited to 'main/src/cgeo/geocaching/sensors')
-rw-r--r--main/src/cgeo/geocaching/sensors/DirectionProvider.java89
-rw-r--r--main/src/cgeo/geocaching/sensors/GeoData.java67
-rw-r--r--main/src/cgeo/geocaching/sensors/GeoDataProvider.java245
-rw-r--r--main/src/cgeo/geocaching/sensors/GeoDirHandler.java128
-rw-r--r--main/src/cgeo/geocaching/sensors/IGeoData.java22
5 files changed, 551 insertions, 0 deletions
diff --git a/main/src/cgeo/geocaching/sensors/DirectionProvider.java b/main/src/cgeo/geocaching/sensors/DirectionProvider.java
new file mode 100644
index 0000000..162f14c
--- /dev/null
+++ b/main/src/cgeo/geocaching/sensors/DirectionProvider.java
@@ -0,0 +1,89 @@
+package cgeo.geocaching.sensors;
+
+import cgeo.geocaching.compatibility.Compatibility;
+
+import rx.Observable;
+import rx.Observable.OnSubscribe;
+import rx.Subscriber;
+import rx.Subscription;
+import rx.observables.ConnectableObservable;
+import rx.subjects.BehaviorSubject;
+import rx.subscriptions.Subscriptions;
+import rx.functions.Action0;
+
+import android.app.Activity;
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+
+public class DirectionProvider implements OnSubscribe<Float> {
+
+ private final SensorManager sensorManager;
+ private final BehaviorSubject<Float> subject = BehaviorSubject.create(0.0f);
+
+ static public Observable<Float> create(final Context context) {
+ return new DirectionProvider((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)).worker.refCount();
+ }
+
+ private DirectionProvider(final SensorManager sensorManager) {
+ this.sensorManager = sensorManager;
+ }
+
+ @Override
+ public void call(final Subscriber<? super Float> subscriber) {
+ subject.distinctUntilChanged().subscribe(subscriber);
+ }
+
+ private final ConnectableObservable<Float> worker = new ConnectableObservable<Float>(this) {
+ @Override
+ public Subscription connect() {
+ @SuppressWarnings("deprecation")
+ // This will be removed when using a new location service. Until then, it is okay to be used.
+ final Sensor defaultSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
+ final SensorEventListener listener = new SensorEventListener() {
+ @Override
+ public void onSensorChanged(final SensorEvent event) {
+ subject.onNext(event.values[0]);
+ }
+
+ @Override
+ public void onAccuracyChanged(final Sensor sensor, final int accuracy) {
+ /*
+ * There is a bug in Android, which apparently causes this method to be called every
+ * time the sensor _value_ changed, even if the _accuracy_ did not change. So logging
+ * this event leads to the log being flooded with multiple entries _per second_,
+ * which I experienced when running cgeo in a building (with GPS and network being
+ * unreliable).
+ *
+ * See for example https://code.google.com/p/android/issues/detail?id=14792
+ */
+
+ //Log.i(Settings.tag, "Compass' accuracy is low (" + accuracy + ")");
+ }
+ };
+
+ sensorManager.registerListener(listener, defaultSensor, SensorManager.SENSOR_DELAY_NORMAL);
+ return Subscriptions.create(new Action0() {
+ @Override
+ public void call() {
+ sensorManager.unregisterListener(listener);
+ }
+ });
+ }
+ };
+
+ /**
+ * Take the phone rotation (through a given activity) in account and adjust the direction.
+ *
+ * @param activity the activity to consider when computing the rotation
+ * @param direction the unadjusted direction in degrees, in the [0, 360[ range
+ * @return the adjusted direction in degrees, in the [0, 360[ range
+ */
+
+ public static float getDirectionNow(final Activity activity, final float direction) {
+ return Compatibility.getDirectionNow(direction, activity);
+ }
+
+}
diff --git a/main/src/cgeo/geocaching/sensors/GeoData.java b/main/src/cgeo/geocaching/sensors/GeoData.java
new file mode 100644
index 0000000..d53ac73
--- /dev/null
+++ b/main/src/cgeo/geocaching/sensors/GeoData.java
@@ -0,0 +1,67 @@
+package cgeo.geocaching.sensors;
+
+import cgeo.geocaching.enumerations.LocationProviderType;
+import cgeo.geocaching.geopoint.Geopoint;
+
+import android.location.Location;
+import android.location.LocationManager;
+
+class GeoData extends Location implements IGeoData {
+ public final boolean gpsEnabled;
+ public final int satellitesVisible;
+ public final int satellitesFixed;
+ public final boolean pseudoLocation;
+
+ GeoData(final Location location, final boolean gpsEnabled, final int satellitesVisible, final int satellitesFixed, final boolean pseudoLocation) {
+ super(location);
+ this.gpsEnabled = gpsEnabled;
+ this.satellitesVisible = satellitesVisible;
+ this.satellitesFixed = satellitesFixed;
+ this.pseudoLocation = pseudoLocation;
+ }
+
+ @Override
+ public Location getLocation() {
+ return this;
+ }
+
+ private static LocationProviderType getLocationProviderType(final String provider) {
+ if (provider.equals(LocationManager.GPS_PROVIDER)) {
+ return LocationProviderType.GPS;
+ }
+ if (provider.equals(LocationManager.NETWORK_PROVIDER)) {
+ return LocationProviderType.NETWORK;
+ }
+ return LocationProviderType.LAST;
+ }
+
+ @Override
+ public LocationProviderType getLocationProvider() {
+ return getLocationProviderType(getProvider());
+ }
+
+ @Override
+ public Geopoint getCoords() {
+ return new Geopoint(this);
+ }
+
+ @Override
+ public boolean getGpsEnabled() {
+ return gpsEnabled;
+ }
+
+ @Override
+ public int getSatellitesVisible() {
+ return satellitesVisible;
+ }
+
+ @Override
+ public int getSatellitesFixed() {
+ return satellitesFixed;
+ }
+
+ @Override
+ public boolean isPseudoLocation() {
+ return pseudoLocation;
+ }
+}
diff --git a/main/src/cgeo/geocaching/sensors/GeoDataProvider.java b/main/src/cgeo/geocaching/sensors/GeoDataProvider.java
new file mode 100644
index 0000000..004a687
--- /dev/null
+++ b/main/src/cgeo/geocaching/sensors/GeoDataProvider.java
@@ -0,0 +1,245 @@
+package cgeo.geocaching.sensors;
+
+import cgeo.geocaching.utils.Log;
+
+import org.apache.commons.lang3.StringUtils;
+import rx.Observable;
+import rx.Observable.OnSubscribe;
+import rx.Subscriber;
+import rx.Subscription;
+import rx.functions.Action0;
+import rx.observables.ConnectableObservable;
+import rx.subjects.BehaviorSubject;
+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;
+
+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 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);
+ }
+ }
+
+ return Subscriptions.create(new Action0() {
+ @Override
+ public void call() {
+ geoManager.removeUpdates(networkListener);
+ geoManager.removeUpdates(gpsListener);
+ geoManager.removeGpsStatusListener(gpsStatusListener);
+ }
+ });
+ }
+ };
+
+ 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);
+ }
+
+}
diff --git a/main/src/cgeo/geocaching/sensors/GeoDirHandler.java b/main/src/cgeo/geocaching/sensors/GeoDirHandler.java
new file mode 100644
index 0000000..146ff76
--- /dev/null
+++ b/main/src/cgeo/geocaching/sensors/GeoDirHandler.java
@@ -0,0 +1,128 @@
+package cgeo.geocaching.sensors;
+
+import cgeo.geocaching.CgeoApplication;
+import cgeo.geocaching.settings.Settings;
+
+import rx.Scheduler;
+import rx.Subscription;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.functions.Action1;
+import rx.schedulers.Schedulers;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * GeoData and Direction handler.
+ * <p>
+ * To use this class, override at least one of {@link #updateDirection(float)} or {@link #updateGeoData(IGeoData)}. You
+ * need to start the handler using one of
+ * <ul>
+ * <li>{@link #startDir()}</li>
+ * <li>{@link #startGeo()}</li>
+ * <li>{@link #startGeoAndDir()}</li>
+ * </ul>
+ * A good place might be the {@code onResume} method of the Activity. Stop the Handler accordingly in {@code onPause}.
+ * </p>
+ */
+public abstract class GeoDirHandler {
+ private static final CgeoApplication app = CgeoApplication.getInstance();
+
+ private Subscription dirSubscription = null;
+ private Subscription geoSubscription = null;
+
+ /**
+ * Update method called when new IGeoData is available.
+ *
+ * @param data
+ * the new data
+ */
+ public void updateGeoData(final IGeoData data) {
+ // Override this in children
+ }
+
+ /**
+ * Update method called when new direction data is available.
+ *
+ * @param direction
+ * the new direction
+ */
+ public void updateDirection(final float direction) {
+ // Override this in children
+ }
+
+ /**
+ * Register the current GeoDirHandler for GeoData information.
+ */
+ public synchronized void startGeo() {
+ geoSubscription = app.currentGeoObject()
+ .subscribeOn(AndroidSchedulers.mainThread())
+ .subscribe(new Action1<IGeoData>() {
+ @Override
+ public void call(final IGeoData geoData) {
+ updateGeoData(geoData);
+ }
+ });
+ }
+
+ /**
+ * Register the current GeoDirHandler for direction information if the preferences
+ * allow it.
+ */
+ public synchronized void startDir() {
+ if (Settings.isUseCompass()) {
+ dirSubscription = app.currentDirObject()
+ .subscribeOn(AndroidSchedulers.mainThread())
+ .subscribe(new Action1<Float>() {
+ @Override
+ public void call(final Float direction) {
+ updateDirection(direction);
+ }
+ });
+ }
+ }
+
+ /**
+ * Register the current GeoDirHandler for GeoData and direction information (if the
+ * preferences allow it).
+ */
+ public void startGeoAndDir() {
+ startGeo();
+ startDir();
+ }
+
+ /**
+ * Unregister the current GeoDirHandler for GeoData information.
+ */
+ public synchronized void stopGeo() {
+ // Delay the unsubscription by 2.5 seconds, so that another activity has
+ // the time to subscribe and the GPS receiver will not be turned down.
+ if (geoSubscription != null) {
+ final Subscription subscription = geoSubscription;
+ geoSubscription = null;
+ Schedulers.newThread().schedule(new Action1<Scheduler.Inner>() {
+ @Override
+ public void call(final Scheduler.Inner inner) {
+ subscription.unsubscribe();
+ }
+ }, 2500, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ /**
+ * Unregister the current GeoDirHandler for direction information.
+ */
+ public synchronized void stopDir() {
+ if (dirSubscription != null) {
+ dirSubscription.unsubscribe();
+ dirSubscription = null;
+ }
+ }
+
+ /**
+ * Unregister the current GeoDirHandler for GeoData and direction information.
+ */
+ public void stopGeoAndDir() {
+ stopGeo();
+ stopDir();
+ }
+}
diff --git a/main/src/cgeo/geocaching/sensors/IGeoData.java b/main/src/cgeo/geocaching/sensors/IGeoData.java
new file mode 100644
index 0000000..5b4f046
--- /dev/null
+++ b/main/src/cgeo/geocaching/sensors/IGeoData.java
@@ -0,0 +1,22 @@
+package cgeo.geocaching.sensors;
+
+import cgeo.geocaching.enumerations.LocationProviderType;
+import cgeo.geocaching.geopoint.Geopoint;
+
+import android.location.Location;
+
+public interface IGeoData {
+
+ public Location getLocation();
+ public LocationProviderType getLocationProvider();
+
+ public boolean isPseudoLocation();
+
+ public Geopoint getCoords();
+ public float getBearing();
+ public float getSpeed();
+ public float getAccuracy();
+ public boolean getGpsEnabled();
+ public int getSatellitesVisible();
+ public int getSatellitesFixed();
+}