Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1ba101f8 authored by Steve Howard's avatar Steve Howard
Browse files

Improve accelerometer-based orientation sensing.

There were three main complains about orientation sensing:
* Switching to landscape when putting a device down on a table (or picking it up)
* Changing orientation due to road bumps or vehicle vibrations while in a car dock
* Switching to upside-down too easily

This change includes three primary enhancements.

First, we run the accelerometer output through a lowpass filter before considering its orientation.  This avoids glitches due to brief phone movement, particularly when the phone hits a table.  The filter uses a very low default time constant of 200ms to retain responsiveness (note the samping period is ~200ms, so the effect of this filtering is pretty mild).  At tilt angles beyond 45 degrees, however, we increase the time constant to 600ms, which helps greatly with avoiding glitches picking the phone up from a table.  It does introduce some sluggishness when rotating while the phone is tilted back, i.e. being used in one's lap.

It's also worth mentioning that the accelerometer output on Sapphire appears to be pre-lowpass-filtered with a time constant of around 500ms, making this less necessary on that device, but the added effect doesn't noticeably degrade user experience in my opinion.

Second, we check the magnitude of the raw accelerometer output.  If it deviates from the strength of gravity by more than one m/s^2, we distrust the data, since that implies the device is under external acceleration and the sensor data doesn't accurately reflect orientation.  This helps avoid glitches due to shocks and vibrations, as in the car dock scenario.  However, rather than ignore the data entirely, we filter it with a very high time constant (5 sec).  As a result, if the device is rotated while vibrating, even if we never pick up a clean sample, we will eventually detect the orientation switch.  Of course, with a sampling period of 200ms, we're prone to aliasing, but that seems like a highly unlikely corner case.

Third, we restrict transitions to upside-down orientation to a much narrower range, both in terms of orientation and tilt.  This should prevent upside-down mode from activating in most cases where it's not desired.

I also updated a lot of stale documentation, added a lot of documentation, and cleaned up a lot of the code, so as to make this (often subtle) code as transparent as possible.
parent 9245bf85
Loading
Loading
Loading
Loading
+161 −73
Original line number Original line Diff line number Diff line
@@ -57,8 +57,12 @@ public abstract class WindowOrientationListener {
     * {@link android.hardware.SensorManager SensorManager}). Use the default
     * {@link android.hardware.SensorManager SensorManager}). Use the default
     * value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL 
     * value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL 
     * SENSOR_DELAY_NORMAL} for simple screen orientation change detection.
     * 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.
     */
     */
    public WindowOrientationListener(Context context, int rate) {
    private WindowOrientationListener(Context context, int rate) {
        mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
        mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
        mRate = rate;
        mRate = rate;
        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
@@ -107,94 +111,179 @@ public abstract class WindowOrientationListener {
    }
    }
    
    
    class SensorEventListenerImpl implements SensorEventListener {
    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_X = 0;
        private static final int _DATA_Y = 1;
        private static final int _DATA_Y = 1;
        private static final int _DATA_Z = 2;
        private static final int _DATA_Z = 2;
        // Angle around x-asis that's considered almost too vertical. Beyond

        // this angle will not result in any orientation changes. f phone faces uses,
        // Internal aliases for the four orientation states.  ROTATION_0 = default portrait mode,
        // the device is leaning backward.
        // ROTATION_90 = left side of device facing the sky, etc.
        private static final int PIVOT_UPPER = 65;
        private static final int ROTATION_0 = 0;
        // Angle about x-axis that's considered negative vertical. Beyond this
        private static final int ROTATION_90 = 1;
        // angle will not result in any orientation changes. If phone faces uses,
        private static final int ROTATION_180 = 2;
        // the device is leaning forward.
        private static final int ROTATION_270 = 3;
        private static final int PIVOT_LOWER = -10;

        static final int ROTATION_0 = 0;
        // Current orientation state
        static final int ROTATION_90 = 1;
        private int mRotation = ROTATION_0;
        static final int ROTATION_180 = 2;

        static final int ROTATION_270 = 3;
        // Mapping our internal aliases into actual Surface rotation values
        int mRotation = ROTATION_0;
        private final int[] SURFACE_ROTATIONS = new int[] {Surface.ROTATION_0, Surface.ROTATION_90,

                Surface.ROTATION_180, Surface.ROTATION_270};
        // Threshold values defined for device rotation positions

        // follow order ROTATION_0 .. ROTATION_270
        // Threshold ranges of orientation angle to transition into other orientation states.
        final int THRESHOLDS[][][] = new int[][][] {
        // The first list is for transitions from ROTATION_0, the next for ROTATION_90, etc.
            {{60, 135}, {135, 225}, {225, 300}},
        // ROTATE_TO defines the orientation each threshold range transitions to, and must be kept
                {{0, 45}, {45, 135}, {135, 210}, {330, 360}},
        // in sync with this.
                {{0, 45}, {45, 120}, {240, 315}, {315, 360}},
        // The thresholds are nearly regular -- we generally transition about the halfway point
                {{0, 30}, {150, 225}, {225, 315}, {315, 360}}
        // between two states with a swing of 30 degreees for hysteresis.  For ROTATION_180,
        // however, we enforce stricter thresholds, pushing the thresholds 15 degrees closer to 180.
        private final int[][][] THRESHOLDS = new int[][][] {
                {{60, 165}, {165, 195}, {195, 300}},
                {{0, 45}, {45, 165}, {165, 195}, {330, 360}},
                {{0, 45}, {45, 135}, {225, 315}, {315, 360}},
                {{0, 30}, {165, 195}, {195, 315}, {315, 360}}
        };
        };


        // Transform rotation ranges based on THRESHOLDS. This
        // See THRESHOLDS
        // has to be in step with THESHOLDS
        private final int[][] ROTATE_TO = new int[][] {
        final int ROTATE_TO[][] = new int[][] {
                {ROTATION_270, ROTATION_180, ROTATION_90},
                {ROTATION_270, ROTATION_180, ROTATION_90},
                {ROTATION_0, ROTATION_270, ROTATION_180, ROTATION_0},
                {ROTATION_0, ROTATION_270, ROTATION_180, ROTATION_0},
                {ROTATION_0, ROTATION_270, ROTATION_90, ROTATION_0},
                {ROTATION_0, ROTATION_270, ROTATION_90, ROTATION_0},
                {ROTATION_0, ROTATION_180, ROTATION_90, ROTATION_0}
                {ROTATION_0, ROTATION_180, ROTATION_90, ROTATION_0}
        };
        };


        // Mapping into actual Surface rotation values
        // Maximum absolute tilt angle at which to consider orientation changes.  Beyond this (i.e.
        final int TRANSFORM_ROTATIONS[] = new int[]{Surface.ROTATION_0,
        // when screen is facing the sky or ground), we refuse to make any orientation changes.
                Surface.ROTATION_90, Surface.ROTATION_180, Surface.ROTATION_270};
        private static final int MAX_TILT = 65;

        // Additional limits on tilt angle to transition to each new orientation.  We ignore all
        // vectors with tilt beyond MAX_TILT, but we can set stricter limits on transition to a
        // particular orientation here.
        private final int[] MAX_TRANSITION_TILT = new int[] {MAX_TILT, MAX_TILT, 40, MAX_TILT};

        // 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 = 45;

        // 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 int MAX_DEVIATION_FROM_GRAVITY = 1;

        // 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 = 200;
        // When device is partially tilted towards the sky or ground
        private static final int TILTED_TIME_CONSTANT_MS = 600;
        // When device is under external acceleration, i.e. not just gravity.  We heavily distrust
        // such readings.
        private static final int ACCELERATING_TIME_CONSTANT_MS = 5000;

        private static final float DEFAULT_LOWPASS_ALPHA =
            (float) SAMPLING_PERIOD_MS / (DEFAULT_TIME_CONSTANT_MS + SAMPLING_PERIOD_MS);
        private static final float TILTED_LOWPASS_ALPHA =
            (float) SAMPLING_PERIOD_MS / (TILTED_TIME_CONSTANT_MS + SAMPLING_PERIOD_MS);
        private static final float ACCELERATING_LOWPASS_ALPHA =
            (float) SAMPLING_PERIOD_MS / (ACCELERATING_TIME_CONSTANT_MS + SAMPLING_PERIOD_MS);

        // The low-pass filtered accelerometer data
        private float[] mFilteredVector = new float[] {0, 0, 0};


        int getCurrentRotation() {
        int getCurrentRotation() {
            return TRANSFORM_ROTATIONS[mRotation];
            return SURFACE_ROTATIONS[mRotation];
        }
        }


        private void calculateNewRotation(int orientation, int zyangle) {
        private void calculateNewRotation(int orientation, int tiltAngle) {
            if (localLOGV) Log.i(TAG, orientation + ", " + zyangle + ", " + mRotation);
            if (localLOGV) Log.i(TAG, orientation + ", " + tiltAngle + ", " + mRotation);
            int rangeArr[][] = THRESHOLDS[mRotation];
            int thresholdRanges[][] = THRESHOLDS[mRotation];
            int row = -1;
            int row = -1;
            for (int i = 0; i < rangeArr.length; i++) {
            for (int i = 0; i < thresholdRanges.length; i++) {
                if ((orientation >= rangeArr[i][0]) && (orientation < rangeArr[i][1])) {
                if (orientation >= thresholdRanges[i][0] && orientation < thresholdRanges[i][1]) {
                    row = i;
                    row = i;
                    break;
                    break;
                }
                }
            }
            }
            if (row != -1) {
            if (row == -1) return; // no matching transition
                // Find new rotation based on current rotation value.

                // This also takes care of irregular rotations as well.
            int rotation = ROTATE_TO[mRotation][row];
            int rotation = ROTATE_TO[mRotation][row];
            if (tiltAngle > MAX_TRANSITION_TILT[rotation]) {
                // tilted too far flat to go to this rotation
                return;
            }

            if (localLOGV) Log.i(TAG, " new rotation = " + rotation);
            if (localLOGV) Log.i(TAG, " new rotation = " + rotation);
                if (rotation != mRotation) {
            mRotation = rotation;
            mRotation = rotation;
                    // Trigger orientation change
            onOrientationChanged(SURFACE_ROTATIONS[rotation]);
                    onOrientationChanged(TRANSFORM_ROTATIONS[rotation]);
        }
        }

        private float lowpassFilter(float newValue, float oldValue, float alpha) {
            return alpha * newValue + (1 - alpha) * oldValue;
        }
        }

        private float vectorMagnitude(float x, float y, float z) {
            return (float) Math.sqrt(x*x + y*y + z*z);
        }

        /**
         * Absolute angle between upVector and the x-y plane (the plane of the screen), in [0, 90].
         * 90 degrees = screen facing the sky or ground.
         */
        private float tiltAngle(float z, float magnitude) {
            return Math.abs((float) Math.asin(z / magnitude) * RADIANS_TO_DEGREES);
        }
        }


        public void onSensorChanged(SensorEvent event) {
        public void onSensorChanged(SensorEvent event) {
            float[] values = event.values;
            // the vector given in the SensorEvent points straight up (towards the sky) under ideal
            float X = values[_DATA_X];
            // conditions (the phone is not accelerating).  i'll call this upVector elsewhere.
            float Y = values[_DATA_Y];
            float x = event.values[_DATA_X];
            float Z = values[_DATA_Z];
            float y = event.values[_DATA_Y];
            float OneEightyOverPi = 57.29577957855f;
            float z = event.values[_DATA_Z];
            float gravity = (float) Math.sqrt(X*X+Y*Y+Z*Z);
            float magnitude = vectorMagnitude(x, y, z);
            float zyangle = (float)Math.asin(Z/gravity)*OneEightyOverPi;
            float deviation = Math.abs(magnitude - SensorManager.STANDARD_GRAVITY);
            if ((zyangle <= PIVOT_UPPER) && (zyangle >= PIVOT_LOWER)) {
            float tiltAngle = tiltAngle(z, magnitude);
                // Check orientation only if the phone is flat enough

                // Don't trust the angle if the magnitude is small compared to the y value
            float alpha = DEFAULT_LOWPASS_ALPHA;
                float angle = (float)Math.atan2(Y, -X) * OneEightyOverPi;
            if (tiltAngle > MAX_TILT) {
                int orientation = 90 - Math.round(angle);
                return;
                // normalize to 0 - 359 range
            } else if (deviation > MAX_DEVIATION_FROM_GRAVITY) {
                while (orientation >= 360) {
                alpha = ACCELERATING_LOWPASS_ALPHA;
                    orientation -= 360;
            } else if (tiltAngle > PARTIAL_TILT) {
                }
                alpha = TILTED_LOWPASS_ALPHA;
                while (orientation < 0) {
                    orientation += 360;
            }
            }
                calculateNewRotation(orientation, Math.round(zyangle));

            x = mFilteredVector[0] = lowpassFilter(x, mFilteredVector[0], alpha);
            y = mFilteredVector[1] = lowpassFilter(y, mFilteredVector[1], alpha);
            z = mFilteredVector[2] = lowpassFilter(z, mFilteredVector[2], alpha);
            magnitude = vectorMagnitude(x, y, z);
            tiltAngle = tiltAngle(z, magnitude);

            // Angle between the x-y projection of upVector and the +y-axis, increasing
            // counter-clockwise.
            // 0 degrees = speaker end towards the sky
            // 90 degrees = left edge of device towards the sky
            float orientationAngle = (float) Math.atan2(-x, y) * RADIANS_TO_DEGREES;
            int orientation = Math.round(orientationAngle);
            // atan2 returns (-180, 180]; normalize to [0, 360)
            if (orientation < 0) {
                orientation += 360;
            }
            }
            calculateNewRotation(orientation, Math.round(tiltAngle));
        }
        }


        public void onAccuracyChanged(Sensor sensor, int accuracy) {
        public void onAccuracyChanged(Sensor sensor, int accuracy) {
@@ -211,10 +300,9 @@ public abstract class WindowOrientationListener {


    /**
    /**
     * Called when the rotation view of the device has changed.
     * Called when the rotation view of the device has changed.
     * Can be either Surface.ROTATION_90 or Surface.ROTATION_0.
     * @param rotation The new orientation of the device.
     *
     *
     *  @see #ORIENTATION_UNKNOWN
     * @param rotation The new orientation of the device, one of the Surface.ROTATION_* constants.
     * @see Surface
     */
     */
    abstract public void onOrientationChanged(int rotation);
    abstract public void onOrientationChanged(int rotation);
}
}