package cgeo.geocaching.sensors; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.playservices.LocationProvider; import cgeo.geocaching.sensors.GpsStatusProvider.Status; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.AngleUtils; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.RxUtils; import android.app.Application; import android.content.Context; import android.support.annotation.NonNull; import java.util.concurrent.atomic.AtomicBoolean; import io.reactivex.Observable; import io.reactivex.functions.Consumer; import io.reactivex.functions.Function; import io.reactivex.functions.Predicate; public class Sensors { private Observable<GeoData> geoDataObservable; private Observable<GeoData> geoDataObservableLowPower; private Observable<Float> directionObservable; private final Observable<Status> gpsStatusObservable; @NonNull private volatile GeoData currentGeo = GeoData.DUMMY_LOCATION; private volatile float currentDirection = 0.0f; private final boolean hasCompassCapabilities; private static class InstanceHolder { static final Sensors INSTANCE = new Sensors(); } private final Consumer<GeoData> rememberGeodataAction = new Consumer<GeoData>() { @Override public void accept(final GeoData geoData) { currentGeo = geoData; } }; private final Consumer<Float> onNextrememberDirectionAction = new Consumer<Float>() { @Override public void accept(final Float direction) { currentDirection = direction; } }; private Sensors() { final Application application = CgeoApplication.getInstance(); gpsStatusObservable = GpsStatusProvider.create(application).replay(1).refCount(); final Context context = application.getApplicationContext(); hasCompassCapabilities = RotationProvider.hasRotationSensor(context) || OrientationProvider.hasOrientationSensor(context) || MagnetometerAndAccelerometerProvider.hasMagnetometerAndAccelerometerSensors(context); } public static Sensors getInstance() { return InstanceHolder.INSTANCE; } private final Function<Throwable, Observable<GeoData>> fallbackToGeodataProvider = new Function<Throwable, Observable<GeoData>>() { @Override public Observable<GeoData> apply(final Throwable throwable) { Log.e("Cannot use Play Services location provider, falling back to GeoDataProvider", throwable); Settings.setUseGooglePlayServices(false); return GeoDataProvider.create(CgeoApplication.getInstance()); } }; public void setupGeoDataObservables(final boolean useGooglePlayServices, final boolean useLowPowerLocation) { final Application application = CgeoApplication.getInstance(); if (useGooglePlayServices) { geoDataObservable = LocationProvider.getMostPrecise(application).onErrorResumeNext(fallbackToGeodataProvider).doOnNext(rememberGeodataAction); if (useLowPowerLocation) { geoDataObservableLowPower = LocationProvider.getLowPower(application).doOnNext(rememberGeodataAction).onErrorResumeNext(geoDataObservable); } else { geoDataObservableLowPower = geoDataObservable; } } else { geoDataObservable = RxUtils.rememberLast(GeoDataProvider.create(application).doOnNext(rememberGeodataAction), null); geoDataObservableLowPower = geoDataObservable; } } private static final Function<GeoData, Float> GPS_TO_DIRECTION = new Function<GeoData, Float>() { @Override public Float apply(final GeoData geoData) { return AngleUtils.reverseDirectionNow(geoData.getBearing()); } }; public void setupDirectionObservable() { // If we have no magnetic sensor, there is no point in trying to setup any, we will always get the direction from the GPS. if (!hasCompassCapabilities) { Log.i("No compass capabilities, using only the GPS for the orientation"); directionObservable = RxUtils.rememberLast(geoDataObservableLowPower.map(GPS_TO_DIRECTION).doOnNext(onNextrememberDirectionAction), 0f); return; } // Combine the magnetic direction observable with the GPS when compass is disabled or speed is high enough. final AtomicBoolean useDirectionFromGps = new AtomicBoolean(false); // On some devices, the orientation sensor (Xperia and S4 running Lollipop) seems to have been deprecated for real. // Use the rotation sensor if it is available unless the orientatation sensor is forced by the user. // After updating Moto G there is no rotation sensor anymore. Use magnetic field and accelerometer instead. final Observable<Float> sensorDirectionObservable; final Application application = CgeoApplication.getInstance(); if (Settings.useOrientationSensor(application)) { sensorDirectionObservable = OrientationProvider.create(application); } else if (RotationProvider.hasRotationSensor(application)) { sensorDirectionObservable = RotationProvider.create(application); } else { sensorDirectionObservable = MagnetometerAndAccelerometerProvider.create(application); } final Observable<Float> magneticDirectionObservable = sensorDirectionObservable.onErrorResumeNext(new Function<Throwable, Observable<Float>>() { @Override public Observable<Float> apply(final Throwable throwable) { Log.e("Device orientation is not available due to sensors error, disabling compass", throwable); Settings.setUseCompass(false); return Observable.<Float>never().startWith(0.0f); } }).filter(new Predicate<Float>() { @Override public boolean test(final Float aFloat) { return Settings.isUseCompass() && !useDirectionFromGps.get(); } }); final Observable<Float> directionFromGpsObservable = geoDataObservableLowPower.filter(new Predicate<GeoData>() { @Override public boolean test(final GeoData geoData) { final boolean useGps = geoData.getSpeed() > 5.0f; useDirectionFromGps.set(useGps); return useGps || !Settings.isUseCompass(); } }).map(GPS_TO_DIRECTION); directionObservable = RxUtils.rememberLast(Observable.merge(magneticDirectionObservable, directionFromGpsObservable).doOnNext(onNextrememberDirectionAction), 0f); } public Observable<GeoData> geoDataObservable(final boolean lowPower) { return lowPower ? geoDataObservableLowPower : geoDataObservable; } public Observable<Float> directionObservable() { return directionObservable; } public Observable<Status> gpsStatusObservable() { return gpsStatusObservable; } @NonNull public GeoData currentGeo() { return currentGeo; } public float currentDirection() { return currentDirection; } public boolean hasCompassCapabilities() { return hasCompassCapabilities; } }