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

Commit 5f531ae6 authored by Steve Howard's avatar Steve Howard
Browse files

Slight improvement (hopefully) to orientation sensing.

Since orientation sensing has been an issue for numerous users, I
decided a spend a little time experimenting with some possible
improvements.  I've settled on a couple major changes:

* Perform all lowpass filtering in spherical coordinates, not
  cartesian.  Since the rotations are what we're really concerned
  with, this makes more sense and gives more consistent results.

* Introduce a system of tracking "distrust" in the current data, based
  on external acceleration and on tilt.  The basic idea is after a
  signal of unreliable data -- repeated acceleration or
  nearly-horizontal tilt -- we wait for things to "stabilize" for some
  number of ticks before we start trusting the data again.  This is an
  extension of the basic lowpass filtering.  One simple example is
  after the phone is picked up off a table, we ignore the first few
  readings.  Another example is while the phone is under external
  acceleration for a while (i.e. in a car mount on a rough road), if a
  single "good" reading comes in, we distrust it, under the assumption
  that it was probably just a lucky reading (i.e. the magnitude
  happened to be close to that of gravity by chance).

These changes have allowed me to relax other constraints, such as the
filtering time constants, the maximum deviation from gravity, and the
max tilt before we start distrusting data.

The net effect is that orientation changes happen more quickly and can
happen under a wider variety of conditions, but false changes due to
tilt and acceleration are still avoided well.  I think the improvement
is subtle, but it's the best I've come up with in my limited time.

I've also included some refactoring and additonal comments to try and
further clarify the (somewhat twisted) logic.

Change-Id: I34c7297bd2061fae8317ffefd32a85c7538a3efb
parent 5d46ce24
Loading
Loading
Loading
Loading
+188 −57
Original line number Diff line number Diff line
@@ -68,7 +68,7 @@ public abstract class WindowOrientationListener {
        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        if (mSensor != null) {
            // Create listener only if sensors do exist
            mSensorEventListener = new SensorEventListenerImpl();
            mSensorEventListener = new SensorEventListenerImpl(this);
        }
    }

@@ -110,7 +110,34 @@ public abstract class WindowOrientationListener {
        return -1;
    }

    class SensorEventListenerImpl implements SensorEventListener {
    /**
     * 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 tilt and orientation angles to avoid "twitchy" behavior.
     *
     *  - 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 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 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.
     *
     * Details are explained below.
     */
    static 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);

@@ -125,54 +152,50 @@ public abstract class WindowOrientationListener {
        private static final int ROTATION_90 = 1;
        private static final int ROTATION_270 = 2;

        // Current orientation state
        private int mRotation = ROTATION_0;

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

        // Threshold ranges of orientation angle to transition into other orientation states.
        // The first list is for transitions from ROTATION_0, the next for ROTATION_90, etc.
        // ROTATE_TO defines the orientation each threshold range transitions to, and must be kept
        // in sync with this.
        // The thresholds are nearly regular -- we generally transition about the halfway point
        // between two states with a swing of 30 degrees for hysteresis.  For ROTATION_180,
        // however, we enforce stricter thresholds, pushing the thresholds 15 degrees closer to 180.
        private final int[][][] THRESHOLDS = new int[][][] {
        // 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}},
                {{0, 30}, {195, 315}, {315, 360}}
        };

        // See THRESHOLDS
        private final int[][] ROTATE_TO = new int[][] {
                {ROTATION_270, ROTATION_90},
        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},
        };

        // Maximum absolute tilt angle at which to consider orientation changes.  Beyond this (i.e.
        // when screen is facing the sky or ground), we refuse to make any orientation changes.
        private static final int MAX_TILT = 65;
        // 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
        // vectors with tilt beyond MAX_TILT, but we can set stricter limits on transition to a
        // data with tilt beyond MAX_TILT, but we can set stricter limits on transitions to a
        // particular orientation here.
        private final int[] MAX_TRANSITION_TILT = new int[] {MAX_TILT, MAX_TILT, MAX_TILT};
        private static final int[] MAX_TRANSITION_TILT = new int[] {MAX_TILT, 65, 65};

        // 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;
        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 int MAX_DEVIATION_FROM_GRAVITY = 1;
        private static final float MAX_DEVIATION_FROM_GRAVITY = 1.5f;

        // Actual sampling period corresponding to SensorManager.SENSOR_DELAY_NORMAL.  There's no
        // way to get this information from SensorManager.
@@ -185,28 +208,46 @@ public abstract class WindowOrientationListener {
        // background.

        // When device is near-vertical (screen approximately facing the horizon)
        private static final int DEFAULT_TIME_CONSTANT_MS = 200;
        private static final int DEFAULT_TIME_CONSTANT_MS = 50;
        // When device is partially tilted towards the sky or ground
        private static final int TILTED_TIME_CONSTANT_MS = 600;
        private static final int TILTED_TIME_CONSTANT_MS = 300;
        // 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 int ACCELERATING_TIME_CONSTANT_MS = 2000;

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

        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 low-pass filtered accelerometer data
        private float[] mFilteredVector = new float[] {0, 0, 0};
        public SensorEventListenerImpl(WindowOrientationListener orientationListener) {
            mOrientationListener = orientationListener;
        }

        private static float computeLowpassAlpha(int timeConstantMs) {
            return (float) SAMPLING_PERIOD_MS / (timeConstantMs + SAMPLING_PERIOD_MS);
        }

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

        private void calculateNewRotation(int orientation, int tiltAngle) {
        private void calculateNewRotation(float orientation, float tiltAngle) {
            if (localLOGV) Log.i(TAG, orientation + ", " + tiltAngle + ", " + mRotation);
            int thresholdRanges[][] = THRESHOLDS[mRotation];
            int row = -1;
@@ -226,7 +267,7 @@ public abstract class WindowOrientationListener {

            if (localLOGV) Log.i(TAG, " new rotation = " + rotation);
            mRotation = rotation;
            onOrientationChanged(SURFACE_ROTATIONS[rotation]);
            mOrientationListener.onOrientationChanged(getCurrentRotation());
        }

        private float lowpassFilter(float newValue, float oldValue, float alpha) {
@@ -238,11 +279,11 @@ public abstract class WindowOrientationListener {
        }

        /**
         * 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.
         * 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 Math.abs((float) Math.asin(z / magnitude) * RADIANS_TO_DEGREES);
            return (float) Math.asin(z / magnitude) * RADIANS_TO_DEGREES;
        }

        public void onSensorChanged(SensorEvent event) {
@@ -253,34 +294,124 @@ public abstract class WindowOrientationListener {
            float z = event.values[_DATA_Z];
            float magnitude = vectorMagnitude(x, y, z);
            float deviation = Math.abs(magnitude - SensorManager.STANDARD_GRAVITY);
            float tiltAngle = tiltAngle(z, magnitude);

            handleAccelerationDistrust(deviation);

            // only filter tilt when we're accelerating
            float alpha = 1;
            if (mAccelerationDistrust > 0) {
                alpha = ACCELERATING_LOWPASS_ALPHA;
            }
            float newTiltAngle = tiltAngle(z, magnitude);
            mTiltAngle = lowpassFilter(newTiltAngle, mTiltAngle, alpha);

            float absoluteTilt = Math.abs(mTiltAngle);
            if (checkFullyTilted(absoluteTilt)) {
                return; // when fully tilted, ignore orientation entirely
            }

            float newOrientationAngle = computeNewOrientation(x, y);
            filterOrientation(absoluteTilt, newOrientationAngle);
            calculateNewRotation(mOrientationAngle, absoluteTilt);
        }

        /**
         * 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
         */
        private void handleAccelerationDistrust(float deviation) {
            if (deviation > MAX_DEVIATION_FROM_GRAVITY) {
                if (mAccelerationDistrust < 5) {
                    mAccelerationDistrust++;
                }
            } else if (mAccelerationDistrust > 0) {
                mAccelerationDistrust--;
            }
        }

        /**
         * 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
         * @return true if the phone is fully tilted
         */
        private boolean checkFullyTilted(float absoluteTilt) {
            boolean fullyTilted = absoluteTilt > MAX_TILT;
            if (fullyTilted) {
                if (mRotation == ROTATION_0) {
                    mOrientationAngle = 0;
                } else if (mRotation == ROTATION_90) {
                    mOrientationAngle = 90;
                } else { // ROTATION_270
                    mOrientationAngle = 270;
                }

                if (mTiltDistrust < 3) {
                    mTiltDistrust = 3;
                }
            } else if (mTiltDistrust > 0) {
                mTiltDistrust--;
            }
            return fullyTilted;
        }

        /**
         * 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
         */
        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;
        }

        /**
         * Compute a new filtered orientation angle.
         */
        private void filterOrientation(float absoluteTilt, float orientationAngle) {
            float alpha = DEFAULT_LOWPASS_ALPHA;
            if (tiltAngle > MAX_TILT) {
                return;
            } else if (deviation > MAX_DEVIATION_FROM_GRAVITY) {
            if (mTiltDistrust > 0 || mAccelerationDistrust > 1) {
                // when fully tilted, or under more than a transient acceleration, distrust heavily
                alpha = ACCELERATING_LOWPASS_ALPHA;
            } else if (tiltAngle > PARTIAL_TILT) {
            } else if (absoluteTilt > PARTIAL_TILT || mAccelerationDistrust == 1) {
                // when tilted partway, or under transient acceleration, distrust lightly
                alpha = TILTED_LOWPASS_ALPHA;
            }

            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;
            // 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;
            }
            calculateNewRotation(orientation, Math.round(tiltAngle));
        }

        public void onAccuracyChanged(Sensor sensor, int accuracy) {