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

Commit 9fbf00cb authored by Steve Howard's avatar Steve Howard Committed by Android (Google) Code Review
Browse files

Merge "Slight improvement (hopefully) to orientation sensing." into gingerbread

parents 3b0d3d51 5f531ae6
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) {