From 4519f07e9c6b993fbe7a3d3df24d71d9450a54f1 Mon Sep 17 00:00:00 2001 From: Jeff Brown Date: Sun, 23 Jan 2011 13:16:01 -0800 Subject: New orientation listener. The objective in this listener is to be more careful about the signal processing to prevent spurious orientation changes and to make all of the tweakable factors physically meaningful. The calibration is defined in terms of time constants and does not assume a particular discrete sampling rate. This is useful because it allows us to change the accelerometer sampling interval if desired without having to change the calibration. Moreover, the accelerometer sampling interval can vary +/- 20ms from one sample to the next even in normal circumstances. Proposed orientation changes are weighted by confidence factors that vary exponentially in relation to how close the device is to the ideal orientation change posture (screen is vertical, angle is exactly at the midpoint of the orientation quadrant, and no external acceleration beside gravity). When not in an ideal posture, the device takes proportionally longer to settle into a new orientation state. Added a little tool to plot the log output of the WindowOrientationListener. Check the README for more information about how to use it. Change-Id: I787f02d03582ff26367df65eda8d9ce85c5cb343 --- .../android/view/WindowOrientationListener.java | 766 ++++++++++++--------- 1 file changed, 446 insertions(+), 320 deletions(-) (limited to 'core/java/android/view') diff --git a/core/java/android/view/WindowOrientationListener.java b/core/java/android/view/WindowOrientationListener.java index 6095a64..62d3e6a 100755 --- a/core/java/android/view/WindowOrientationListener.java +++ b/core/java/android/view/WindowOrientationListener.java @@ -23,6 +23,7 @@ import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.util.Config; import android.util.Log; +import android.util.Slog; /** * A special helper class used by the WindowManager @@ -33,17 +34,27 @@ import android.util.Log; * "App/Activity/Screen Orientation" to ensure that all orientation * modes still work correctly. * + * You can also visualize the behavior of the WindowOrientationListener by + * enabling the window orientation listener log using the Development Settings + * in the Dev Tools application (Development.apk) + * and running frameworks/base/tools/orientationplot/orientationplot.py. + * + * More information about how to tune this algorithm in + * frameworks/base/tools/orientationplot/README.txt. + * * @hide */ public abstract class WindowOrientationListener { private static final String TAG = "WindowOrientationListener"; private static final boolean DEBUG = false; private static final boolean localLOGV = DEBUG || Config.DEBUG; + private SensorManager mSensorManager; - private boolean mEnabled = false; + private boolean mEnabled; private int mRate; private Sensor mSensor; private SensorEventListenerImpl mSensorEventListener; + boolean mLogEnabled; /** * Creates a new WindowOrientationListener. @@ -51,7 +62,7 @@ public abstract class WindowOrientationListener { * @param context for the WindowOrientationListener. */ public WindowOrientationListener(Context context) { - this(context, SensorManager.SENSOR_DELAY_NORMAL); + this(context, SensorManager.SENSOR_DELAY_UI); } /** @@ -63,9 +74,7 @@ public abstract class WindowOrientationListener { * value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL * SENSOR_DELAY_NORMAL} for simple screen orientation change detection. * - * This constructor is private since no one uses it and making it public would complicate - * things, since the lowpass filtering code depends on the actual sampling period, and there's - * no way to get the period from SensorManager based on the rate constant. + * This constructor is private since no one uses it. */ private WindowOrientationListener(Context context, int rate) { mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); @@ -108,12 +117,11 @@ public abstract class WindowOrientationListener { } } - public void setAllow180Rotation(boolean allowed) { - if (mSensorEventListener != null) { - mSensorEventListener.setAllow180Rotation(allowed); - } - } - + /** + * Gets the current orientation. + * @param lastRotation + * @return + */ public int getCurrentRotation(int lastRotation) { if (mEnabled) { return mSensorEventListener.getCurrentRotation(lastRotation); @@ -122,375 +130,493 @@ public abstract class WindowOrientationListener { } /** + * Returns true if sensor is enabled and false otherwise + */ + public boolean canDetectOrientation() { + return mSensor != null; + } + + /** + * Called when the rotation view of the device has changed. + * + * @param rotation The new orientation of the device, one of the Surface.ROTATION_* constants. + * @see Surface + */ + public abstract void onOrientationChanged(int rotation); + + /** + * Enables or disables the window orientation listener logging for use with + * the orientationplot.py tool. + * Logging is usually enabled via Development Settings. (See class comments.) + * @param enable True to enable logging. + */ + public void setLogEnabled(boolean enable) { + mLogEnabled = enable; + } + + /** * This class filters the raw accelerometer data and tries to detect actual changes in * orientation. This is a very ill-defined problem so there are a lot of tweakable parameters, * but here's the outline: * - * - Convert the acceleromter vector from cartesian to spherical coordinates. Since we're - * dealing with rotation of the device, this is the sensible coordinate system to work in. The - * zenith direction is the Z-axis, i.e. the direction the screen is facing. The radial distance - * is referred to as the magnitude below. The elevation angle is referred to as the "tilt" - * below. The azimuth angle is referred to as the "orientation" below (and the azimuth axis is - * the Y-axis). See http://en.wikipedia.org/wiki/Spherical_coordinate_system for reference. + * - Low-pass filter the accelerometer vector in cartesian coordinates. We do it in + * cartesian space because the orientation calculations are sensitive to the + * absolute magnitude of the acceleration. In particular, there are singularities + * in the calculation as the magnitude approaches 0. By performing the low-pass + * filtering early, we can eliminate high-frequency impulses systematically. + * + * - Convert the acceleromter vector from cartesian to spherical coordinates. + * Since we're dealing with rotation of the device, this is the sensible coordinate + * system to work in. The zenith direction is the Z-axis, the direction the screen + * is facing. The radial distance is referred to as the magnitude below. + * The elevation angle is referred to as the "tilt" below. + * The azimuth angle is referred to as the "orientation" below (and the azimuth axis is + * the Y-axis). + * See http://en.wikipedia.org/wiki/Spherical_coordinate_system for reference. + * + * - If the tilt angle is too close to horizontal (near 90 or -90 degrees), do nothing. + * The orientation angle is not meaningful when the device is nearly horizontal. + * The tilt angle thresholds are set differently for each orientation and different + * limits are applied when the device is facing down as opposed to when it is facing + * forward or facing up. * - * - Low-pass filter the tilt and orientation angles to avoid "twitchy" behavior. + * - When the orientation angle reaches a certain threshold, consider transitioning + * to the corresponding orientation. These thresholds have some hysteresis built-in + * to avoid oscillations between adjacent orientations. * - * - When the orientation angle reaches a certain threshold, transition to the corresponding - * orientation. These thresholds have some hysteresis built-in to avoid oscillation. + * - Use the magnitude to judge the confidence of the orientation. + * Under ideal conditions, the magnitude should equal to that of gravity. When it + * differs significantly, we know the device is under external acceleration and + * we can't trust the data. * - * - Use the magnitude to judge the accuracy of the data. Under ideal conditions, the magnitude - * should equal to that of gravity. When it differs significantly, we know the device is under - * external acceleration and we can't trust the data. + * - Use the tilt angle to judge the confidence of the orientation. + * When the tilt angle is high in absolute value then the device is nearly flat + * so small physical movements produce large changes in orientation angle. + * This can be the case when the device is being picked up from a table. * - * - Use the tilt angle to judge the accuracy of orientation data. When the tilt angle is high - * in magnitude, we distrust the orientation data, because when the device is nearly flat, small - * physical movements produce large changes in orientation angle. + * - Use the orientation angle to judge the confidence of the orientation. + * The close the orientation angle is to the canonical orientation angle, the better. * - * Details are explained below. + * - Based on the aggregate confidence, we determine how long we want to wait for + * the new orientation to settle. This is accomplished by integrating the confidence + * for each orientation over time. When a threshold integration sum is reached + * then we actually change orientations. + * + * Details are explained inline. */ - static class SensorEventListenerImpl implements SensorEventListener { + static final class SensorEventListenerImpl implements SensorEventListener { // We work with all angles in degrees in this class. private static final float RADIANS_TO_DEGREES = (float) (180 / Math.PI); - // Indices into SensorEvent.values - private static final int _DATA_X = 0; - private static final int _DATA_Y = 1; - private static final int _DATA_Z = 2; - - // Internal aliases for the four orientation states. ROTATION_0 = default portrait mode, - // ROTATION_90 = right side of device facing the sky, etc. - private static final int ROTATION_0 = 0; - private static final int ROTATION_90 = 1; - private static final int ROTATION_270 = 2; - private static final int ROTATION_180 = 3; - - // Mapping our internal aliases into actual Surface rotation values - private static final int[] INTERNAL_TO_SURFACE_ROTATION = new int[] { - Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_270, - Surface.ROTATION_180}; - - // Mapping Surface rotation values to internal aliases. - private static final int[] SURFACE_TO_INTERNAL_ROTATION = new int[] { - ROTATION_0, ROTATION_90, ROTATION_180, ROTATION_270}; - - // Threshold ranges of orientation angle to transition into other orientation states. - // The first list is for transitions from ROTATION_0, ROTATION_90, ROTATION_270, - // and then ROTATION_180. - // ROTATE_TO defines the orientation each threshold range transitions to, and must be kept - // in sync with this. - // We generally transition about the halfway point between two states with a swing of 30 - // degrees for hysteresis. - private static final int[][][] THRESHOLDS = new int[][][] { - {{60, 180}, {180, 300}}, - {{0, 30}, {195, 315}, {315, 360}}, - {{0, 45}, {45, 165}, {330, 360}}, - - // Handle situation where we are currently doing 180 rotation - // but that is no longer allowed. - {{0, 45}, {45, 135}, {135, 225}, {225, 315}, {315, 360}}, - }; - // See THRESHOLDS - private static final int[][] ROTATE_TO = new int[][] { - {ROTATION_90, ROTATION_270}, - {ROTATION_0, ROTATION_270, ROTATION_0}, - {ROTATION_0, ROTATION_90, ROTATION_0}, - {ROTATION_0, ROTATION_90, ROTATION_0, ROTATION_270, ROTATION_0}, - }; + // Indices into SensorEvent.values for the accelerometer sensor. + private static final int ACCELEROMETER_DATA_X = 0; + private static final int ACCELEROMETER_DATA_Y = 1; + private static final int ACCELEROMETER_DATA_Z = 2; + + // Rotation constants. + // These are the same as Surface rotation constants with the addition of a 5th + // unknown state when we are not confident about the proporsed orientation. + // One important property of these constants is that they are equal to the + // orientation angle itself divided by 90. We use this fact to map + // back and forth between orientation angles and rotation values. + private static final int ROTATION_UNKNOWN = -1; + //private static final int ROTATION_0 = Surface.ROTATION_0; // 0 + //private static final int ROTATION_90 = Surface.ROTATION_90; // 1 + //private static final int ROTATION_180 = Surface.ROTATION_180; // 2 + //private static final int ROTATION_270 = Surface.ROTATION_270; // 3 + + private final WindowOrientationListener mOrientationListener; + + private int mRotation = ROTATION_UNKNOWN; + + /* State for first order low-pass filtering of accelerometer data. + * See http://en.wikipedia.org/wiki/Low-pass_filter#Discrete-time_realization for + * signal processing background. + */ - // Thresholds that allow all 4 orientations. - private static final int[][][] THRESHOLDS_WITH_180 = new int[][][] { - {{60, 165}, {165, 195}, {195, 300}}, - {{0, 30}, {165, 195}, {195, 315}, {315, 360}}, - {{0, 45}, {45, 165}, {165, 195}, {330, 360}}, - {{0, 45}, {45, 135}, {225, 315}, {315, 360}}, - }; - // See THRESHOLDS_WITH_180 - private static final int[][] ROTATE_TO_WITH_180 = new int[][] { - {ROTATION_90, ROTATION_180, ROTATION_270}, - {ROTATION_0, ROTATION_180, ROTATION_90, ROTATION_0}, - {ROTATION_0, ROTATION_270, ROTATION_180, ROTATION_0}, - {ROTATION_0, ROTATION_90, ROTATION_270, ROTATION_0}, - }; + private long mLastTimestamp = Long.MAX_VALUE; // in nanoseconds + private float mLastFilteredX, mLastFilteredY, mLastFilteredZ; + + // The maximum sample inter-arrival time in milliseconds. + // If the acceleration samples are further apart than this amount in time, we reset the + // state of the low-pass filter and orientation properties. This helps to handle + // boundary conditions when the device is turned on, wakes from suspend or there is + // a significant gap in samples. + private static final float MAX_FILTER_DELTA_TIME_MS = 1000; + + // The acceleration filter cutoff frequency. + // This is the frequency at which signals are attenuated by 3dB (half the passband power). + // Each successive octave beyond this frequency is attenuated by an additional 6dB. + // + // We choose the cutoff frequency such that impulses and vibrational noise + // (think car dock) is suppressed. However, this filtering does not eliminate + // all possible sources of orientation ambiguity so we also rely on a dynamic + // settle time for establishing a new orientation. Filtering adds latency + // inversely proportional to the cutoff frequency so we don't want to make + // it too small or we can lose hundreds of milliseconds of responsiveness. + private static final float FILTER_CUTOFF_FREQUENCY_HZ = 1f; + private static final float FILTER_TIME_CONSTANT_MS = (float)(500.0f + / (Math.PI * FILTER_CUTOFF_FREQUENCY_HZ)); // t = 1 / (2pi * Fc) * 1000ms + + // The filter gain. + // We choose a value slightly less than unity to avoid numerical instabilities due + // to floating-point error accumulation. + private static final float FILTER_GAIN = 0.999f; + + /* State for orientation detection. */ + + // Thresholds for minimum and maximum allowable deviation from gravity. + // + // If the device is undergoing external acceleration (being bumped, in a car + // that is turning around a corner or a plane taking off) then the magnitude + // may be substantially more or less than gravity. This can skew our orientation + // detection by making us think that up is pointed in a different direction. + // + // Conversely, if the device is in freefall, then there will be no gravity to + // measure at all. This is problematic because we cannot detect the orientation + // without gravity to tell us which way is up. A magnitude near 0 produces + // singularities in the tilt and orientation calculations. + // + // In both cases, we postpone choosing an orientation. + private static final float MIN_ACCELERATION_MAGNITUDE = + SensorManager.STANDARD_GRAVITY * 0.5f; + private static final float MAX_ACCELERATION_MAGNITUDE = + SensorManager.STANDARD_GRAVITY * 1.5f; // Maximum absolute tilt angle at which to consider orientation data. Beyond this (i.e. // when screen is facing the sky or ground), we completely ignore orientation data. private static final int MAX_TILT = 75; - // Additional limits on tilt angle to transition to each new orientation. We ignore all - // data with tilt beyond MAX_TILT, but we can set stricter limits on transitions to a - // particular orientation here. - private static final int[] MAX_TRANSITION_TILT = new int[] {MAX_TILT, 65, 65, 40}; - - // Between this tilt angle and MAX_TILT, we'll allow orientation changes, but we'll filter - // with a higher time constant, making us less sensitive to change. This primarily helps - // prevent momentary orientation changes when placing a device on a table from the side (or - // picking one up). - private static final int PARTIAL_TILT = 50; - - // Maximum allowable deviation of the magnitude of the sensor vector from that of gravity, - // in m/s^2. Beyond this, we assume the phone is under external forces and we can't trust - // the sensor data. However, under constantly vibrating conditions (think car mount), we - // still want to pick up changes, so rather than ignore the data, we filter it with a very - // high time constant. - private static final float MAX_DEVIATION_FROM_GRAVITY = 1.5f; - - // Minimum acceleration considered, in m/s^2. Below this threshold sensor noise will have - // significant impact on the calculations and in case of the vector (0, 0, 0) there is no - // defined rotation or tilt at all. Low or zero readings can happen when space travelling - // or free falling, but more commonly when shaking or getting bad readings from the sensor. - // The accelerometer is turned off when not used and polling it too soon after it is - // turned on may result in (0, 0, 0). - private static final float MIN_ABS_ACCELERATION = 1.5f; - - // Actual sampling period corresponding to SensorManager.SENSOR_DELAY_NORMAL. There's no - // way to get this information from SensorManager. - // Note the actual period is generally 3-30ms larger than this depending on the device, but - // that's not enough to significantly skew our results. - private static final int SAMPLING_PERIOD_MS = 200; - - // The following time constants are all used in low-pass filtering the accelerometer output. - // See http://en.wikipedia.org/wiki/Low-pass_filter#Discrete-time_realization for - // background. - - // When device is near-vertical (screen approximately facing the horizon) - private static final int DEFAULT_TIME_CONSTANT_MS = 100; - // When device is partially tilted towards the sky or ground - private static final int TILTED_TIME_CONSTANT_MS = 500; - // When device is under external acceleration, i.e. not just gravity. We heavily distrust - // such readings. - private static final int ACCELERATING_TIME_CONSTANT_MS = 2000; - - private static final float DEFAULT_LOWPASS_ALPHA = - computeLowpassAlpha(DEFAULT_TIME_CONSTANT_MS); - private static final float TILTED_LOWPASS_ALPHA = - computeLowpassAlpha(TILTED_TIME_CONSTANT_MS); - private static final float ACCELERATING_LOWPASS_ALPHA = - computeLowpassAlpha(ACCELERATING_TIME_CONSTANT_MS); - - private boolean mAllow180Rotation = false; - - private WindowOrientationListener mOrientationListener; - private int mRotation = ROTATION_0; // Current orientation state - private float mTiltAngle = 0; // low-pass filtered - private float mOrientationAngle = 0; // low-pass filtered - - /* - * Each "distrust" counter represents our current level of distrust in the data based on - * a certain signal. For each data point that is deemed unreliable based on that signal, - * the counter increases; otherwise, the counter decreases. Exact rules vary. - */ - private int mAccelerationDistrust = 0; // based on magnitude != gravity - private int mTiltDistrust = 0; // based on tilt close to +/- 90 degrees + // The tilt angle range in degrees for each orientation. + // Beyond these tilt angles, we don't even consider transitioning into the + // specified orientation. We place more stringent requirements on unnatural + // orientations than natural ones to make it less likely to accidentally transition + // into those states. + // The first value of each pair is negative so it applies a limit when the device is + // facing down (overhead reading in bed). + // The second value of each pair is positive so it applies a limit when the device is + // facing up (resting on a table). + // The ideal tilt angle is 0 (when the device is vertical) so the limits establish + // how close to vertical the device must be in order to change orientation. + private static final int[][] TILT_TOLERANCE = new int[][] { + /* ROTATION_0 */ { -20, 75 }, + /* ROTATION_90 */ { -20, 70 }, + /* ROTATION_180 */ { -20, 65 }, + /* ROTATION_270 */ { -20, 70 } + }; - public SensorEventListenerImpl(WindowOrientationListener orientationListener) { - mOrientationListener = orientationListener; - } + // The gap angle in degrees between adjacent orientation angles for hysteresis. + // This creates a "dead zone" between the current orientation and a proposed + // adjacent orientation. No orientation proposal is made when the orientation + // angle is within the gap between the current orientation and the adjacent + // orientation. + private static final int ADJACENT_ORIENTATION_ANGLE_GAP = 30; - private static float computeLowpassAlpha(int timeConstantMs) { - return (float) SAMPLING_PERIOD_MS / (timeConstantMs + SAMPLING_PERIOD_MS); - } + // The confidence scale factors for angle, tilt and magnitude. + // When the distance between the actual value and the ideal value is the + // specified delta, orientation transitions will take twice as long as they would + // in the ideal case. Increasing or decreasing the delta has an exponential effect + // on each factor's influence over the transition time. - void setAllow180Rotation(boolean allowed) { - mAllow180Rotation = allowed; - } + // Transition takes 2x longer when angle is 30 degrees from ideal orientation angle. + private static final float ORIENTATION_ANGLE_CONFIDENCE_SCALE = + confidenceScaleFromDelta(30); - int getCurrentRotation(int lastRotation) { - if (mTiltDistrust > 0) { - // we really don't know the current orientation, so trust what's currently displayed - mRotation = SURFACE_TO_INTERNAL_ROTATION[lastRotation]; - } - return INTERNAL_TO_SURFACE_ROTATION[mRotation]; - } + // Transition takes 2x longer when tilt is 45 degrees from vertical. + private static final float TILT_ANGLE_CONFIDENCE_SCALE = confidenceScaleFromDelta(45); - private void calculateNewRotation(float orientation, float tiltAngle) { - if (localLOGV) Log.i(TAG, orientation + ", " + tiltAngle + ", " + mRotation); - final boolean allow180Rotation = mAllow180Rotation; - int thresholdRanges[][] = allow180Rotation - ? THRESHOLDS_WITH_180[mRotation] : THRESHOLDS[mRotation]; - int row = -1; - for (int i = 0; i < thresholdRanges.length; i++) { - if (orientation >= thresholdRanges[i][0] && orientation < thresholdRanges[i][1]) { - row = i; - break; - } - } - if (row == -1) return; // no matching transition + // Transition takes 2x longer when acceleration is 0.25 Gs. + private static final float MAGNITUDE_CONFIDENCE_SCALE = confidenceScaleFromDelta( + SensorManager.STANDARD_GRAVITY * 0.25f); - int rotation = allow180Rotation - ? ROTATE_TO_WITH_180[mRotation][row] : ROTATE_TO[mRotation][row]; - if (tiltAngle > MAX_TRANSITION_TILT[rotation]) { - // tilted too far flat to go to this rotation - return; - } + // The number of milliseconds for which a new orientation must be stable before + // we perform an orientation change under ideal conditions. It will take + // proportionally longer than this to effect an orientation change when + // the proposed orientation confidence is low. + private static final float ORIENTATION_SETTLE_TIME_MS = 250; - if (localLOGV) Log.i(TAG, "orientation " + orientation + " gives new rotation = " - + rotation); - mRotation = rotation; - mOrientationListener.onOrientationChanged(INTERNAL_TO_SURFACE_ROTATION[mRotation]); - } + // The confidence that we have abount effecting each orientation change. + // When one of these values exceeds 1.0, we have determined our new orientation! + private float mConfidence[] = new float[4]; - private float lowpassFilter(float newValue, float oldValue, float alpha) { - return alpha * newValue + (1 - alpha) * oldValue; + public SensorEventListenerImpl(WindowOrientationListener orientationListener) { + mOrientationListener = orientationListener; } - private float vectorMagnitude(float x, float y, float z) { - return (float) Math.sqrt(x*x + y*y + z*z); + public int getCurrentRotation(int lastRotation) { + return mRotation != ROTATION_UNKNOWN ? mRotation : lastRotation; } - /** - * Angle between upVector and the x-y plane (the plane of the screen), in [-90, 90]. - * +/- 90 degrees = screen facing the sky or ground. - */ - private float tiltAngle(float z, float magnitude) { - return (float) Math.asin(z / magnitude) * RADIANS_TO_DEGREES; + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { } + @Override public void onSensorChanged(SensorEvent event) { - // the vector given in the SensorEvent points straight up (towards the sky) under ideal - // conditions (the phone is not accelerating). i'll call this upVector elsewhere. - float x = event.values[_DATA_X]; - float y = event.values[_DATA_Y]; - float z = event.values[_DATA_Z]; - float magnitude = vectorMagnitude(x, y, z); - float deviation = Math.abs(magnitude - SensorManager.STANDARD_GRAVITY); - - handleAccelerationDistrust(deviation); - if (magnitude < MIN_ABS_ACCELERATION) { - return; // Ignore tilt and orientation when (0, 0, 0) or low reading + final boolean log = mOrientationListener.mLogEnabled; + + // The vector given in the SensorEvent points straight up (towards the sky) under ideal + // conditions (the phone is not accelerating). I'll call this up vector elsewhere. + float x = event.values[ACCELEROMETER_DATA_X]; + float y = event.values[ACCELEROMETER_DATA_Y]; + float z = event.values[ACCELEROMETER_DATA_Z]; + + if (log) { + Slog.v(TAG, "Raw acceleration vector: " + + "x=" + x + ", y=" + y + ", z=" + z); } - // only filter tilt when we're accelerating - float alpha = 1; - if (mAccelerationDistrust > 0) { - alpha = ACCELERATING_LOWPASS_ALPHA; + // Apply a low-pass filter to the acceleration up vector in cartesian space. + // Reset the orientation listener state if the samples are too far apart in time + // or when we see values of (0, 0, 0) which indicates that we polled the + // accelerometer too soon after turning it on and we don't have any data yet. + final float timeDeltaMS = (event.timestamp - mLastTimestamp) * 0.000001f; + boolean skipSample; + if (timeDeltaMS <= 0 || timeDeltaMS > MAX_FILTER_DELTA_TIME_MS + || (x == 0 && y == 0 && z == 0)) { + if (log) { + Slog.v(TAG, "Resetting orientation listener."); + } + for (int i = 0; i < 4; i++) { + mConfidence[i] = 0; + } + skipSample = true; + } else { + final float alpha = timeDeltaMS + / (FILTER_TIME_CONSTANT_MS + timeDeltaMS) * FILTER_GAIN; + x = alpha * (x - mLastFilteredX) + mLastFilteredX; + y = alpha * (y - mLastFilteredY) + mLastFilteredY; + z = alpha * (z - mLastFilteredZ) + mLastFilteredZ; + if (log) { + Slog.v(TAG, "Filtered acceleration vector: " + + "x=" + x + ", y=" + y + ", z=" + z); + } + skipSample = false; } - float newTiltAngle = tiltAngle(z, magnitude); - mTiltAngle = lowpassFilter(newTiltAngle, mTiltAngle, alpha); + mLastTimestamp = event.timestamp; + mLastFilteredX = x; + mLastFilteredY = y; + mLastFilteredZ = z; + + boolean orientationChanged = false; + if (!skipSample) { + // Determine a proposed orientation based on the currently available data. + int proposedOrientation = ROTATION_UNKNOWN; + float combinedConfidence = 1.0f; + + // Calculate the magnitude of the acceleration vector. + final float magnitude = (float) Math.sqrt(x * x + y * y + z * z); + if (magnitude < MIN_ACCELERATION_MAGNITUDE + || magnitude > MAX_ACCELERATION_MAGNITUDE) { + if (log) { + Slog.v(TAG, "Ignoring sensor data, magnitude out of range: " + + "magnitude=" + magnitude); + } + } else { + // Calculate the tilt angle. + // This is the angle between the up vector and the x-y plane (the plane of + // the screen) in a range of [-90, 90] degrees. + // -90 degrees: screen horizontal and facing the ground (overhead) + // 0 degrees: screen vertical + // 90 degrees: screen horizontal and facing the sky (on table) + final int tiltAngle = (int) Math.round( + Math.asin(z / magnitude) * RADIANS_TO_DEGREES); + + // If the tilt angle is too close to horizontal then we cannot determine + // the orientation angle of the screen. + if (Math.abs(tiltAngle) > MAX_TILT) { + if (log) { + Slog.v(TAG, "Ignoring sensor data, tilt angle too high: " + + "magnitude=" + magnitude + ", tiltAngle=" + tiltAngle); + } + } else { + // Calculate the orientation angle. + // This is the angle between the x-y projection of the up vector onto + // the +y-axis, increasing clockwise in a range of [0, 360] degrees. + int orientationAngle = (int) Math.round( + -Math.atan2(-x, y) * RADIANS_TO_DEGREES); + if (orientationAngle < 0) { + // atan2 returns [-180, 180]; normalize to [0, 360] + orientationAngle += 360; + } + + // Find the nearest orientation. + // An orientation of 0 can have a nearest angle of 0 or 360 depending + // on which is closer to the measured orientation angle. We leave the + // nearest angle at 360 in that case since it makes the delta calculation + // for orientation angle confidence easier below. + int nearestOrientation = (orientationAngle + 45) / 90; + int nearestOrientationAngle = nearestOrientation * 90; + if (nearestOrientation == 4) { + nearestOrientation = 0; + } + + // Determine the proposed orientation. + // The confidence of the proposal is 1.0 when it is ideal and it + // decays exponentially as the proposal moves further from the ideal + // angle, tilt and magnitude of the proposed orientation. + if (isTiltAngleAcceptable(nearestOrientation, tiltAngle) + && isOrientationAngleAcceptable(nearestOrientation, + orientationAngle)) { + proposedOrientation = nearestOrientation; + + final float idealOrientationAngle = nearestOrientationAngle; + final float orientationConfidence = confidence(orientationAngle, + idealOrientationAngle, ORIENTATION_ANGLE_CONFIDENCE_SCALE); + + final float idealTiltAngle = 0; + final float tiltConfidence = confidence(tiltAngle, + idealTiltAngle, TILT_ANGLE_CONFIDENCE_SCALE); + + final float idealMagnitude = SensorManager.STANDARD_GRAVITY; + final float magnitudeConfidence = confidence(magnitude, + idealMagnitude, MAGNITUDE_CONFIDENCE_SCALE); + + combinedConfidence = orientationConfidence + * tiltConfidence * magnitudeConfidence; + + if (log) { + Slog.v(TAG, "Proposal: " + + "magnitude=" + magnitude + + ", tiltAngle=" + tiltAngle + + ", orientationAngle=" + orientationAngle + + ", proposedOrientation=" + proposedOrientation + + ", combinedConfidence=" + combinedConfidence + + ", orientationConfidence=" + orientationConfidence + + ", tiltConfidence=" + tiltConfidence + + ", magnitudeConfidence=" + magnitudeConfidence); + } + } else { + if (log) { + Slog.v(TAG, "Ignoring sensor data, no proposal: " + + "magnitude=" + magnitude + ", tiltAngle=" + tiltAngle + + ", orientationAngle=" + orientationAngle); + } + } + } + } - float absoluteTilt = Math.abs(mTiltAngle); - checkFullyTilted(absoluteTilt); - if (mTiltDistrust > 0) { - return; // when fully tilted, ignore orientation entirely + // Sum up the orientation confidence weights. + // Detect an orientation change when the sum reaches 1.0. + final float confidenceAmount = combinedConfidence * timeDeltaMS + / ORIENTATION_SETTLE_TIME_MS; + for (int i = 0; i < 4; i++) { + if (i == proposedOrientation) { + mConfidence[i] += confidenceAmount; + if (mConfidence[i] >= 1.0f) { + mConfidence[i] = 1.0f; + + if (i != mRotation) { + if (log) { + Slog.v(TAG, "Orientation changed! rotation=" + i); + } + mRotation = i; + orientationChanged = true; + } + } + } else { + mConfidence[i] -= confidenceAmount; + if (mConfidence[i] < 0.0f) { + mConfidence[i] = 0.0f; + } + } + } } - float newOrientationAngle = computeNewOrientation(x, y); - filterOrientation(absoluteTilt, newOrientationAngle); - calculateNewRotation(mOrientationAngle, absoluteTilt); + // Write final statistics about where we are in the orientation detection process. + if (log) { + Slog.v(TAG, "Result: rotation=" + mRotation + + ", confidence=[" + + mConfidence[0] + ", " + + mConfidence[1] + ", " + + mConfidence[2] + ", " + + mConfidence[3] + "], timeDeltaMS=" + timeDeltaMS); + } + + // Tell the listener. + if (orientationChanged) { + mOrientationListener.onOrientationChanged(mRotation); + } } /** - * When accelerating, increment distrust; otherwise, decrement distrust. The idea is that - * if a single jolt happens among otherwise good data, we should keep trusting the good - * data. On the other hand, if a series of many bad readings comes in (as if the phone is - * being rapidly shaken), we should wait until things "settle down", i.e. we get a string - * of good readings. - * - * @param deviation absolute difference between the current magnitude and gravity + * Returns true if the tilt angle is acceptable for a proposed + * orientation transition. */ - private void handleAccelerationDistrust(float deviation) { - if (deviation > MAX_DEVIATION_FROM_GRAVITY) { - if (mAccelerationDistrust < 5) { - mAccelerationDistrust++; - } - } else if (mAccelerationDistrust > 0) { - mAccelerationDistrust--; - } + private boolean isTiltAngleAcceptable(int proposedOrientation, + int tiltAngle) { + return tiltAngle >= TILT_TOLERANCE[proposedOrientation][0] + && tiltAngle <= TILT_TOLERANCE[proposedOrientation][1]; } /** - * Check if the phone is tilted towards the sky or ground and handle that appropriately. - * When fully tilted, we automatically push the tilt up to a fixed value; otherwise we - * decrement it. The idea is to distrust the first few readings after the phone gets - * un-tilted, no matter what, i.e. preventing an accidental transition when the phone is - * picked up from a table. - * - * We also reset the orientation angle to the center of the current screen orientation. - * Since there is no real orientation of the phone, we want to ignore the most recent sensor - * data and reset it to this value to avoid a premature transition when the phone starts to - * get un-tilted. - * - * @param absoluteTilt the absolute value of the current tilt angle + * Returns true if the orientation angle is acceptable for a proposed + * orientation transition. + * This function takes into account the gap between adjacent orientations + * for hysteresis. */ - private void checkFullyTilted(float absoluteTilt) { - if (absoluteTilt > MAX_TILT) { - if (mRotation == ROTATION_0) { - mOrientationAngle = 0; - } else if (mRotation == ROTATION_90) { - mOrientationAngle = 90; - } else { // ROTATION_270 - mOrientationAngle = 270; + private boolean isOrientationAngleAcceptable(int proposedOrientation, + int orientationAngle) { + final int currentOrientation = mRotation; + + // If there is no current rotation, then there is no gap. + if (currentOrientation != ROTATION_UNKNOWN) { + // If the proposed orientation is the same or is counter-clockwise adjacent, + // then we set a lower bound on the orientation angle. + // For example, if currentOrientation is ROTATION_0 and proposed is ROTATION_90, + // then we want to check orientationAngle > 45 + GAP / 2. + if (proposedOrientation == currentOrientation + || proposedOrientation == (currentOrientation + 1) % 4) { + int lowerBound = proposedOrientation * 90 - 45 + + ADJACENT_ORIENTATION_ANGLE_GAP / 2; + if (proposedOrientation == 0) { + if (orientationAngle >= 315 && orientationAngle < lowerBound + 360) { + return false; + } + } else { + if (orientationAngle < lowerBound) { + return false; + } + } } - if (mTiltDistrust < 3) { - mTiltDistrust = 3; + // If the proposed orientation is the same or is clockwise adjacent, + // then we set an upper bound on the orientation angle. + // For example, if currentOrientation is ROTATION_0 and proposed is ROTATION_270, + // then we want to check orientationAngle < 315 - GAP / 2. + if (proposedOrientation == currentOrientation + || proposedOrientation == (currentOrientation + 3) % 4) { + int upperBound = proposedOrientation * 90 + 45 + - ADJACENT_ORIENTATION_ANGLE_GAP / 2; + if (proposedOrientation == 0) { + if (orientationAngle <= 45 && orientationAngle > upperBound) { + return false; + } + } else { + if (orientationAngle > upperBound) { + return false; + } + } } - } else if (mTiltDistrust > 0) { - mTiltDistrust--; } + return true; } /** - * Angle between the x-y projection of upVector and the +y-axis, increasing - * clockwise. - * 0 degrees = speaker end towards the sky - * 90 degrees = right edge of device towards the sky + * Calculate an exponentially weighted confidence value in the range [0.0, 1.0]. + * The further the value is from the target, the more the confidence trends to 0. */ - private float computeNewOrientation(float x, float y) { - float orientationAngle = (float) -Math.atan2(-x, y) * RADIANS_TO_DEGREES; - // atan2 returns [-180, 180]; normalize to [0, 360] - if (orientationAngle < 0) { - orientationAngle += 360; - } - return orientationAngle; + private static float confidence(float value, float target, float scale) { + return (float) Math.exp(-Math.abs(value - target) * scale); } /** - * Compute a new filtered orientation angle. + * Calculate a scale factor for the confidence weight exponent. + * The scale value is chosen such that confidence(value, target, scale) == 0.5 + * whenever abs(value - target) == cutoffDelta. */ - private void filterOrientation(float absoluteTilt, float orientationAngle) { - float alpha = DEFAULT_LOWPASS_ALPHA; - if (mAccelerationDistrust > 1) { - // when under more than a transient acceleration, distrust heavily - alpha = ACCELERATING_LOWPASS_ALPHA; - } else if (absoluteTilt > PARTIAL_TILT || mAccelerationDistrust == 1) { - // when tilted partway, or under transient acceleration, distrust lightly - alpha = TILTED_LOWPASS_ALPHA; - } - - // since we're lowpass filtering a value with periodic boundary conditions, we need to - // adjust the new value to filter in the right direction... - float deltaOrientation = orientationAngle - mOrientationAngle; - if (deltaOrientation > 180) { - orientationAngle -= 360; - } else if (deltaOrientation < -180) { - orientationAngle += 360; - } - mOrientationAngle = lowpassFilter(orientationAngle, mOrientationAngle, alpha); - // ...and then adjust back to ensure we're in the range [0, 360] - if (mOrientationAngle > 360) { - mOrientationAngle -= 360; - } else if (mOrientationAngle < 0) { - mOrientationAngle += 360; - } - } - - public void onAccuracyChanged(Sensor sensor, int accuracy) { - + private static float confidenceScaleFromDelta(float cutoffDelta) { + return (float) -Math.log(0.5) / cutoffDelta; } } - - /* - * Returns true if sensor is enabled and false otherwise - */ - public boolean canDetectOrientation() { - return mSensor != null; - } - - /** - * Called when the rotation view of the device has changed. - * - * @param rotation The new orientation of the device, one of the Surface.ROTATION_* constants. - * @see Surface - */ - abstract public void onOrientationChanged(int rotation); } -- cgit v1.1