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