diff options
Diffstat (limited to 'main/src/cgeo/geocaching/sensors')
| -rw-r--r-- | main/src/cgeo/geocaching/sensors/DirectionProvider.java | 89 | ||||
| -rw-r--r-- | main/src/cgeo/geocaching/sensors/GeoData.java | 67 | ||||
| -rw-r--r-- | main/src/cgeo/geocaching/sensors/GeoDataProvider.java | 245 | ||||
| -rw-r--r-- | main/src/cgeo/geocaching/sensors/GeoDirHandler.java | 128 | ||||
| -rw-r--r-- | main/src/cgeo/geocaching/sensors/IGeoData.java | 22 |
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(); +} |
