package cgeo.geocaching.sensors;
import cgeo.geocaching.utils.AndroidRxUtils;
import cgeo.geocaching.utils.Log;
import android.annotation.TargetApi;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
public class RotationProvider {
/**
* On some Samsung devices, {@link SensorManager#getRotationMatrixFromVector} throws an exception if the rotation
* vector has more than 4 elements.
* <p/>
* This will be detected and remembered after the first occurrence of the exception. Concurrent access
* is not a problem as this variable can only go from {@code false} to {@code true} and being {@code false}
* instead of {@code true} is innocuous and will be changed immediately when needed.
*
* @see <a href="http://stackoverflow.com/a/22138449">this Stack Overflow answer</a>
*/
private static boolean isTruncationNeeded = false;
private RotationProvider() {
// Utility class, not to be instantiated
}
@TargetApi(19)
public static Observable<Float> create(final Context context) {
final SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
final Sensor rotationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
if (rotationSensor == null) {
Log.w("RotationProvider: no rotation sensor on this device");
return Observable.error(new RuntimeException("no rotation sensor"));
}
final Observable<Float> observable = Observable.create(new ObservableOnSubscribe<Float>() {
@Override
public void subscribe(final ObservableEmitter<Float> emitter) throws Exception {
final SensorEventListener listener = new SensorEventListener() {
private final float[] rotationMatrix = new float[16];
private final float[] orientation = new float[4];
private final float[] values = new float[4];
@Override
public void onSensorChanged(final SensorEvent event) {
if (isTruncationNeeded) {
// Since only the four first elements are used (and accepted), we truncate the vector.
System.arraycopy(event.values, 0, values, 0, 4);
SensorManager.getRotationMatrixFromVector(rotationMatrix, values);
} else {
try {
SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values);
} catch (final IllegalArgumentException ignored) {
Log.d("installing workaround for mismatched number of values in rotation vector");
// Install workaround and retry
isTruncationNeeded = true;
onSensorChanged(event);
return;
}
}
SensorManager.getOrientation(rotationMatrix, orientation);
emitter.onNext((float) (orientation[0] * 180 / Math.PI));
}
@Override
public void onAccuracyChanged(final Sensor sensor, final int accuracy) {
// empty
}
};
Log.d("RotationProvider: registering listener");
sensorManager.registerListener(listener, rotationSensor, SensorManager.SENSOR_DELAY_NORMAL);
emitter.setDisposable(AndroidRxUtils.disposeOnCallbacksScheduler(new Runnable() {
@Override
public void run() {
Log.d("RotationProvider: unregistering listener");
sensorManager.unregisterListener(listener, rotationSensor);
}
}));
}
});
return observable.subscribeOn(AndroidRxUtils.looperCallbacksScheduler).share();
}
public static boolean hasRotationSensor(final Context context) {
return ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)).getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) != null;
}
}