1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.view;
18
19import android.content.Context;
20import android.hardware.Sensor;
21import android.hardware.SensorEvent;
22import android.hardware.SensorEventListener;
23import android.hardware.SensorManager;
24import android.util.Log;
25import android.util.Slog;
26
27/**
28 * A special helper class used by the WindowManager
29 * for receiving notifications from the SensorManager when
30 * the orientation of the device has changed.
31 *
32 * NOTE: If changing anything here, please run the API demo
33 * "App/Activity/Screen Orientation" to ensure that all orientation
34 * modes still work correctly.
35 *
36 * You can also visualize the behavior of the WindowOrientationListener by
37 * enabling the window orientation listener log using the Development Settings
38 * in the Dev Tools application (Development.apk)
39 * and running frameworks/base/tools/orientationplot/orientationplot.py.
40 *
41 * More information about how to tune this algorithm in
42 * frameworks/base/tools/orientationplot/README.txt.
43 *
44 * @hide
45 */
46public abstract class WindowOrientationListener {
47    private static final String TAG = "WindowOrientationListener";
48    private static final boolean DEBUG = false;
49    private static final boolean localLOGV = DEBUG || false;
50
51    private SensorManager mSensorManager;
52    private boolean mEnabled;
53    private int mRate;
54    private Sensor mSensor;
55    private SensorEventListenerImpl mSensorEventListener;
56    boolean mLogEnabled;
57    int mCurrentRotation = -1;
58
59    /**
60     * Creates a new WindowOrientationListener.
61     *
62     * @param context for the WindowOrientationListener.
63     */
64    public WindowOrientationListener(Context context) {
65        this(context, SensorManager.SENSOR_DELAY_UI);
66    }
67
68    /**
69     * Creates a new WindowOrientationListener.
70     *
71     * @param context for the WindowOrientationListener.
72     * @param rate at which sensor events are processed (see also
73     * {@link android.hardware.SensorManager SensorManager}). Use the default
74     * value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL
75     * SENSOR_DELAY_NORMAL} for simple screen orientation change detection.
76     *
77     * This constructor is private since no one uses it.
78     */
79    private WindowOrientationListener(Context context, int rate) {
80        mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
81        mRate = rate;
82        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
83        if (mSensor != null) {
84            // Create listener only if sensors do exist
85            mSensorEventListener = new SensorEventListenerImpl(this);
86        }
87    }
88
89    /**
90     * Enables the WindowOrientationListener so it will monitor the sensor and call
91     * {@link #onOrientationChanged} when the device orientation changes.
92     */
93    public void enable() {
94        if (mSensor == null) {
95            Log.w(TAG, "Cannot detect sensors. Not enabled");
96            return;
97        }
98        if (mEnabled == false) {
99            if (localLOGV) Log.d(TAG, "WindowOrientationListener enabled");
100            mSensorManager.registerListener(mSensorEventListener, mSensor, mRate);
101            mEnabled = true;
102        }
103    }
104
105    /**
106     * Disables the WindowOrientationListener.
107     */
108    public void disable() {
109        if (mSensor == null) {
110            Log.w(TAG, "Cannot detect sensors. Invalid disable");
111            return;
112        }
113        if (mEnabled == true) {
114            if (localLOGV) Log.d(TAG, "WindowOrientationListener disabled");
115            mSensorManager.unregisterListener(mSensorEventListener);
116            mEnabled = false;
117        }
118    }
119
120    /**
121     * Sets the current rotation.
122     *
123     * @param rotation The current rotation.
124     */
125    public void setCurrentRotation(int rotation) {
126        mCurrentRotation = rotation;
127    }
128
129    /**
130     * Gets the proposed rotation.
131     *
132     * This method only returns a rotation if the orientation listener is certain
133     * of its proposal.  If the rotation is indeterminate, returns -1.
134     *
135     * @return The proposed rotation, or -1 if unknown.
136     */
137    public int getProposedRotation() {
138        if (mEnabled) {
139            return mSensorEventListener.getProposedRotation();
140        }
141        return -1;
142    }
143
144    /**
145     * Returns true if sensor is enabled and false otherwise
146     */
147    public boolean canDetectOrientation() {
148        return mSensor != null;
149    }
150
151    /**
152     * Called when the rotation view of the device has changed.
153     *
154     * This method is called whenever the orientation becomes certain of an orientation.
155     * It is called each time the orientation determination transitions from being
156     * uncertain to being certain again, even if it is the same orientation as before.
157     *
158     * @param rotation The new orientation of the device, one of the Surface.ROTATION_* constants.
159     * @see Surface
160     */
161    public abstract void onProposedRotationChanged(int rotation);
162
163    /**
164     * Enables or disables the window orientation listener logging for use with
165     * the orientationplot.py tool.
166     * Logging is usually enabled via Development Settings.  (See class comments.)
167     * @param enable True to enable logging.
168     */
169    public void setLogEnabled(boolean enable) {
170        mLogEnabled = enable;
171    }
172
173    /**
174     * This class filters the raw accelerometer data and tries to detect actual changes in
175     * orientation. This is a very ill-defined problem so there are a lot of tweakable parameters,
176     * but here's the outline:
177     *
178     *  - Low-pass filter the accelerometer vector in cartesian coordinates.  We do it in
179     *    cartesian space because the orientation calculations are sensitive to the
180     *    absolute magnitude of the acceleration.  In particular, there are singularities
181     *    in the calculation as the magnitude approaches 0.  By performing the low-pass
182     *    filtering early, we can eliminate high-frequency impulses systematically.
183     *
184     *  - Convert the acceleromter vector from cartesian to spherical coordinates.
185     *    Since we're dealing with rotation of the device, this is the sensible coordinate
186     *    system to work in.  The zenith direction is the Z-axis, the direction the screen
187     *    is facing.  The radial distance is referred to as the magnitude below.
188     *    The elevation angle is referred to as the "tilt" below.
189     *    The azimuth angle is referred to as the "orientation" below (and the azimuth axis is
190     *    the Y-axis).
191     *    See http://en.wikipedia.org/wiki/Spherical_coordinate_system for reference.
192     *
193     *  - If the tilt angle is too close to horizontal (near 90 or -90 degrees), do nothing.
194     *    The orientation angle is not meaningful when the device is nearly horizontal.
195     *    The tilt angle thresholds are set differently for each orientation and different
196     *    limits are applied when the device is facing down as opposed to when it is facing
197     *    forward or facing up.
198     *
199     *  - When the orientation angle reaches a certain threshold, consider transitioning
200     *    to the corresponding orientation.  These thresholds have some hysteresis built-in
201     *    to avoid oscillations between adjacent orientations.
202     *
203     *  - Wait for the device to settle for a little bit.  Once that happens, issue the
204     *    new orientation proposal.
205     *
206     * Details are explained inline.
207     */
208    static final class SensorEventListenerImpl implements SensorEventListener {
209        // We work with all angles in degrees in this class.
210        private static final float RADIANS_TO_DEGREES = (float) (180 / Math.PI);
211
212        // Indices into SensorEvent.values for the accelerometer sensor.
213        private static final int ACCELEROMETER_DATA_X = 0;
214        private static final int ACCELEROMETER_DATA_Y = 1;
215        private static final int ACCELEROMETER_DATA_Z = 2;
216
217        private final WindowOrientationListener mOrientationListener;
218
219        /* State for first order low-pass filtering of accelerometer data.
220         * See http://en.wikipedia.org/wiki/Low-pass_filter#Discrete-time_realization for
221         * signal processing background.
222         */
223
224        private long mLastTimestamp = Long.MAX_VALUE; // in nanoseconds
225        private float mLastFilteredX, mLastFilteredY, mLastFilteredZ;
226
227        // The current proposal.  We wait for the proposal to be stable for a
228        // certain amount of time before accepting it.
229        //
230        // The basic idea is to ignore intermediate poses of the device while the
231        // user is picking up, putting down or turning the device.
232        private long mProposalTime;
233        private int mProposalRotation;
234        private long mProposalAgeMS;
235        private boolean mProposalSettled;
236
237        // A historical trace of tilt and orientation angles.  Used to determine whether
238        // the device posture has settled down.
239        private static final int HISTORY_SIZE = 20;
240        private int mHistoryIndex; // index of most recent sample
241        private int mHistoryLength; // length of historical trace
242        private final long[] mHistoryTimestampMS = new long[HISTORY_SIZE];
243        private final float[] mHistoryMagnitudes = new float[HISTORY_SIZE];
244        private final int[] mHistoryTiltAngles = new int[HISTORY_SIZE];
245        private final int[] mHistoryOrientationAngles = new int[HISTORY_SIZE];
246
247        // The maximum sample inter-arrival time in milliseconds.
248        // If the acceleration samples are further apart than this amount in time, we reset the
249        // state of the low-pass filter and orientation properties.  This helps to handle
250        // boundary conditions when the device is turned on, wakes from suspend or there is
251        // a significant gap in samples.
252        private static final float MAX_FILTER_DELTA_TIME_MS = 1000;
253
254        // The acceleration filter time constant.
255        //
256        // This time constant is used to tune the acceleration filter such that
257        // impulses and vibrational noise (think car dock) is suppressed before we
258        // try to calculate the tilt and orientation angles.
259        //
260        // The filter time constant is related to the filter cutoff frequency, which is the
261        // frequency at which signals are attenuated by 3dB (half the passband power).
262        // Each successive octave beyond this frequency is attenuated by an additional 6dB.
263        //
264        // Given a time constant t in seconds, the filter cutoff frequency Fc in Hertz
265        // is given by Fc = 1 / (2pi * t).
266        //
267        // The higher the time constant, the lower the cutoff frequency, so more noise
268        // will be suppressed.
269        //
270        // Filtering adds latency proportional the time constant (inversely proportional
271        // to the cutoff frequency) so we don't want to make the time constant too
272        // large or we can lose responsiveness.
273        private static final float FILTER_TIME_CONSTANT_MS = 100.0f;
274
275        /* State for orientation detection. */
276
277        // Thresholds for minimum and maximum allowable deviation from gravity.
278        //
279        // If the device is undergoing external acceleration (being bumped, in a car
280        // that is turning around a corner or a plane taking off) then the magnitude
281        // may be substantially more or less than gravity.  This can skew our orientation
282        // detection by making us think that up is pointed in a different direction.
283        //
284        // Conversely, if the device is in freefall, then there will be no gravity to
285        // measure at all.  This is problematic because we cannot detect the orientation
286        // without gravity to tell us which way is up.  A magnitude near 0 produces
287        // singularities in the tilt and orientation calculations.
288        //
289        // In both cases, we postpone choosing an orientation.
290        private static final float MIN_ACCELERATION_MAGNITUDE =
291                SensorManager.STANDARD_GRAVITY * 0.5f;
292        private static final float MAX_ACCELERATION_MAGNITUDE =
293            SensorManager.STANDARD_GRAVITY * 1.5f;
294
295        // Maximum absolute tilt angle at which to consider orientation data.  Beyond this (i.e.
296        // when screen is facing the sky or ground), we completely ignore orientation data.
297        private static final int MAX_TILT = 75;
298
299        // The tilt angle range in degrees for each orientation.
300        // Beyond these tilt angles, we don't even consider transitioning into the
301        // specified orientation.  We place more stringent requirements on unnatural
302        // orientations than natural ones to make it less likely to accidentally transition
303        // into those states.
304        // The first value of each pair is negative so it applies a limit when the device is
305        // facing down (overhead reading in bed).
306        // The second value of each pair is positive so it applies a limit when the device is
307        // facing up (resting on a table).
308        // The ideal tilt angle is 0 (when the device is vertical) so the limits establish
309        // how close to vertical the device must be in order to change orientation.
310        private static final int[][] TILT_TOLERANCE = new int[][] {
311            /* ROTATION_0   */ { -25, 70 },
312            /* ROTATION_90  */ { -25, 65 },
313            /* ROTATION_180 */ { -25, 60 },
314            /* ROTATION_270 */ { -25, 65 }
315        };
316
317        // The gap angle in degrees between adjacent orientation angles for hysteresis.
318        // This creates a "dead zone" between the current orientation and a proposed
319        // adjacent orientation.  No orientation proposal is made when the orientation
320        // angle is within the gap between the current orientation and the adjacent
321        // orientation.
322        private static final int ADJACENT_ORIENTATION_ANGLE_GAP = 45;
323
324        // The number of milliseconds for which the device posture must be stable
325        // before we perform an orientation change.  If the device appears to be rotating
326        // (being picked up, put down) then we keep waiting until it settles.
327        private static final int SETTLE_TIME_MIN_MS = 200;
328
329        // The maximum number of milliseconds to wait for the posture to settle before
330        // accepting the current proposal regardless.
331        private static final int SETTLE_TIME_MAX_MS = 500;
332
333        // The maximum change in magnitude that can occur during the settle time.
334        // Tuning this constant particularly helps to filter out situations where the
335        // device is being picked up or put down by the user.
336        private static final float SETTLE_MAGNITUDE_MAX_DELTA =
337                SensorManager.STANDARD_GRAVITY * 0.2f;
338
339        // The maximum change in tilt angle that can occur during the settle time.
340        private static final int SETTLE_TILT_ANGLE_MAX_DELTA = 8;
341
342        // The maximum change in orientation angle that can occur during the settle time.
343        private static final int SETTLE_ORIENTATION_ANGLE_MAX_DELTA = 8;
344
345        public SensorEventListenerImpl(WindowOrientationListener orientationListener) {
346            mOrientationListener = orientationListener;
347        }
348
349        public int getProposedRotation() {
350            return mProposalSettled ? mProposalRotation : -1;
351        }
352
353        @Override
354        public void onAccuracyChanged(Sensor sensor, int accuracy) {
355        }
356
357        @Override
358        public void onSensorChanged(SensorEvent event) {
359            final boolean log = mOrientationListener.mLogEnabled;
360
361            // The vector given in the SensorEvent points straight up (towards the sky) under ideal
362            // conditions (the phone is not accelerating).  I'll call this up vector elsewhere.
363            float x = event.values[ACCELEROMETER_DATA_X];
364            float y = event.values[ACCELEROMETER_DATA_Y];
365            float z = event.values[ACCELEROMETER_DATA_Z];
366
367            if (log) {
368                Slog.v(TAG, "Raw acceleration vector: " +
369                        "x=" + x + ", y=" + y + ", z=" + z);
370            }
371
372            // Apply a low-pass filter to the acceleration up vector in cartesian space.
373            // Reset the orientation listener state if the samples are too far apart in time
374            // or when we see values of (0, 0, 0) which indicates that we polled the
375            // accelerometer too soon after turning it on and we don't have any data yet.
376            final long now = event.timestamp;
377            final float timeDeltaMS = (now - mLastTimestamp) * 0.000001f;
378            boolean skipSample;
379            if (timeDeltaMS <= 0 || timeDeltaMS > MAX_FILTER_DELTA_TIME_MS
380                    || (x == 0 && y == 0 && z == 0)) {
381                if (log) {
382                    Slog.v(TAG, "Resetting orientation listener.");
383                }
384                clearProposal();
385                skipSample = true;
386            } else {
387                final float alpha = timeDeltaMS / (FILTER_TIME_CONSTANT_MS + timeDeltaMS);
388                x = alpha * (x - mLastFilteredX) + mLastFilteredX;
389                y = alpha * (y - mLastFilteredY) + mLastFilteredY;
390                z = alpha * (z - mLastFilteredZ) + mLastFilteredZ;
391                if (log) {
392                    Slog.v(TAG, "Filtered acceleration vector: " +
393                            "x=" + x + ", y=" + y + ", z=" + z);
394                }
395                skipSample = false;
396            }
397            mLastTimestamp = now;
398            mLastFilteredX = x;
399            mLastFilteredY = y;
400            mLastFilteredZ = z;
401
402            final int oldProposedRotation = getProposedRotation();
403            if (!skipSample) {
404                // Calculate the magnitude of the acceleration vector.
405                final float magnitude = (float) Math.sqrt(x * x + y * y + z * z);
406                if (magnitude < MIN_ACCELERATION_MAGNITUDE
407                        || magnitude > MAX_ACCELERATION_MAGNITUDE) {
408                    if (log) {
409                        Slog.v(TAG, "Ignoring sensor data, magnitude out of range: "
410                                + "magnitude=" + magnitude);
411                    }
412                    clearProposal();
413                } else {
414                    // Calculate the tilt angle.
415                    // This is the angle between the up vector and the x-y plane (the plane of
416                    // the screen) in a range of [-90, 90] degrees.
417                    //   -90 degrees: screen horizontal and facing the ground (overhead)
418                    //     0 degrees: screen vertical
419                    //    90 degrees: screen horizontal and facing the sky (on table)
420                    final int tiltAngle = (int) Math.round(
421                            Math.asin(z / magnitude) * RADIANS_TO_DEGREES);
422
423                    // If the tilt angle is too close to horizontal then we cannot determine
424                    // the orientation angle of the screen.
425                    if (Math.abs(tiltAngle) > MAX_TILT) {
426                        if (log) {
427                            Slog.v(TAG, "Ignoring sensor data, tilt angle too high: "
428                                    + "magnitude=" + magnitude + ", tiltAngle=" + tiltAngle);
429                        }
430                        clearProposal();
431                    } else {
432                        // Calculate the orientation angle.
433                        // This is the angle between the x-y projection of the up vector onto
434                        // the +y-axis, increasing clockwise in a range of [0, 360] degrees.
435                        int orientationAngle = (int) Math.round(
436                                -Math.atan2(-x, y) * RADIANS_TO_DEGREES);
437                        if (orientationAngle < 0) {
438                            // atan2 returns [-180, 180]; normalize to [0, 360]
439                            orientationAngle += 360;
440                        }
441
442                        // Find the nearest rotation.
443                        int nearestRotation = (orientationAngle + 45) / 90;
444                        if (nearestRotation == 4) {
445                            nearestRotation = 0;
446                        }
447
448                        // Determine the proposed orientation.
449                        if (!isTiltAngleAcceptable(nearestRotation, tiltAngle)
450                                || !isOrientationAngleAcceptable(nearestRotation,
451                                        orientationAngle)) {
452                            if (log) {
453                                Slog.v(TAG, "Ignoring sensor data, no proposal: "
454                                        + "magnitude=" + magnitude + ", tiltAngle=" + tiltAngle
455                                        + ", orientationAngle=" + orientationAngle);
456                            }
457                            clearProposal();
458                        } else {
459                            if (log) {
460                                Slog.v(TAG, "Proposal: "
461                                        + "magnitude=" + magnitude
462                                        + ", tiltAngle=" + tiltAngle
463                                        + ", orientationAngle=" + orientationAngle
464                                        + ", proposalRotation=" + mProposalRotation);
465                            }
466                            updateProposal(nearestRotation, now / 1000000L,
467                                    magnitude, tiltAngle, orientationAngle);
468                        }
469                    }
470                }
471            }
472
473            // Write final statistics about where we are in the orientation detection process.
474            final int proposedRotation = getProposedRotation();
475            if (log) {
476                final float proposalConfidence = Math.min(
477                        mProposalAgeMS * 1.0f / SETTLE_TIME_MIN_MS, 1.0f);
478                Slog.v(TAG, "Result: currentRotation=" + mOrientationListener.mCurrentRotation
479                        + ", proposedRotation=" + proposedRotation
480                        + ", timeDeltaMS=" + timeDeltaMS
481                        + ", proposalRotation=" + mProposalRotation
482                        + ", proposalAgeMS=" + mProposalAgeMS
483                        + ", proposalConfidence=" + proposalConfidence);
484            }
485
486            // Tell the listener.
487            if (proposedRotation != oldProposedRotation && proposedRotation >= 0) {
488                if (log) {
489                    Slog.v(TAG, "Proposed rotation changed!  proposedRotation=" + proposedRotation
490                            + ", oldProposedRotation=" + oldProposedRotation);
491                }
492                mOrientationListener.onProposedRotationChanged(proposedRotation);
493            }
494        }
495
496        /**
497         * Returns true if the tilt angle is acceptable for a proposed
498         * orientation transition.
499         */
500        private boolean isTiltAngleAcceptable(int proposedRotation,
501                int tiltAngle) {
502            return tiltAngle >= TILT_TOLERANCE[proposedRotation][0]
503                    && tiltAngle <= TILT_TOLERANCE[proposedRotation][1];
504        }
505
506        /**
507         * Returns true if the orientation angle is acceptable for a proposed
508         * orientation transition.
509         *
510         * This function takes into account the gap between adjacent orientations
511         * for hysteresis.
512         */
513        private boolean isOrientationAngleAcceptable(int proposedRotation, int orientationAngle) {
514            // If there is no current rotation, then there is no gap.
515            // The gap is used only to introduce hysteresis among advertised orientation
516            // changes to avoid flapping.
517            final int currentRotation = mOrientationListener.mCurrentRotation;
518            if (currentRotation >= 0) {
519                // If the proposed rotation is the same or is counter-clockwise adjacent,
520                // then we set a lower bound on the orientation angle.
521                // For example, if currentRotation is ROTATION_0 and proposed is ROTATION_90,
522                // then we want to check orientationAngle > 45 + GAP / 2.
523                if (proposedRotation == currentRotation
524                        || proposedRotation == (currentRotation + 1) % 4) {
525                    int lowerBound = proposedRotation * 90 - 45
526                            + ADJACENT_ORIENTATION_ANGLE_GAP / 2;
527                    if (proposedRotation == 0) {
528                        if (orientationAngle >= 315 && orientationAngle < lowerBound + 360) {
529                            return false;
530                        }
531                    } else {
532                        if (orientationAngle < lowerBound) {
533                            return false;
534                        }
535                    }
536                }
537
538                // If the proposed rotation is the same or is clockwise adjacent,
539                // then we set an upper bound on the orientation angle.
540                // For example, if currentRotation is ROTATION_0 and proposed is ROTATION_270,
541                // then we want to check orientationAngle < 315 - GAP / 2.
542                if (proposedRotation == currentRotation
543                        || proposedRotation == (currentRotation + 3) % 4) {
544                    int upperBound = proposedRotation * 90 + 45
545                            - ADJACENT_ORIENTATION_ANGLE_GAP / 2;
546                    if (proposedRotation == 0) {
547                        if (orientationAngle <= 45 && orientationAngle > upperBound) {
548                            return false;
549                        }
550                    } else {
551                        if (orientationAngle > upperBound) {
552                            return false;
553                        }
554                    }
555                }
556            }
557            return true;
558        }
559
560        private void clearProposal() {
561            mProposalRotation = -1;
562            mProposalAgeMS = 0;
563            mProposalSettled = false;
564        }
565
566        private void updateProposal(int rotation, long timestampMS,
567                float magnitude, int tiltAngle, int orientationAngle) {
568            if (mProposalRotation != rotation) {
569                mProposalTime = timestampMS;
570                mProposalRotation = rotation;
571                mHistoryIndex = 0;
572                mHistoryLength = 0;
573            }
574
575            final int index = mHistoryIndex;
576            mHistoryTimestampMS[index] = timestampMS;
577            mHistoryMagnitudes[index] = magnitude;
578            mHistoryTiltAngles[index] = tiltAngle;
579            mHistoryOrientationAngles[index] = orientationAngle;
580            mHistoryIndex = (index + 1) % HISTORY_SIZE;
581            if (mHistoryLength < HISTORY_SIZE) {
582                mHistoryLength += 1;
583            }
584
585            long age = 0;
586            for (int i = 1; i < mHistoryLength; i++) {
587                final int olderIndex = (index + HISTORY_SIZE - i) % HISTORY_SIZE;
588                if (Math.abs(mHistoryMagnitudes[olderIndex] - magnitude)
589                        > SETTLE_MAGNITUDE_MAX_DELTA) {
590                    break;
591                }
592                if (angleAbsoluteDelta(mHistoryTiltAngles[olderIndex],
593                        tiltAngle) > SETTLE_TILT_ANGLE_MAX_DELTA) {
594                    break;
595                }
596                if (angleAbsoluteDelta(mHistoryOrientationAngles[olderIndex],
597                        orientationAngle) > SETTLE_ORIENTATION_ANGLE_MAX_DELTA) {
598                    break;
599                }
600                age = timestampMS - mHistoryTimestampMS[olderIndex];
601                if (age >= SETTLE_TIME_MIN_MS) {
602                    break;
603                }
604            }
605            mProposalAgeMS = age;
606            if (age >= SETTLE_TIME_MIN_MS
607                    || timestampMS - mProposalTime >= SETTLE_TIME_MAX_MS) {
608                mProposalSettled = true;
609            } else {
610                mProposalSettled = false;
611            }
612        }
613
614        private static int angleAbsoluteDelta(int a, int b) {
615            int delta = Math.abs(a - b);
616            if (delta > 180) {
617                delta = 360 - delta;
618            }
619            return delta;
620        }
621    }
622}
623