| /* |
| * Copyright (C) 2008 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.view; |
| |
| import android.content.Context; |
| import android.hardware.Sensor; |
| import android.hardware.SensorEvent; |
| import android.hardware.SensorEventListener; |
| import android.hardware.SensorManager; |
| import android.util.Log; |
| import android.util.Slog; |
| |
| /** |
| * A special helper class used by the WindowManager |
| * for receiving notifications from the SensorManager when |
| * the orientation of the device has changed. |
| * |
| * NOTE: If changing anything here, please run the API demo |
| * "App/Activity/Screen Orientation" to ensure that all orientation |
| * modes still work correctly. |
| * |
| * You can also visualize the behavior of the WindowOrientationListener by |
| * enabling the window orientation listener log using the Development Settings |
| * in the Dev Tools application (Development.apk) |
| * and running frameworks/base/tools/orientationplot/orientationplot.py. |
| * |
| * More information about how to tune this algorithm in |
| * frameworks/base/tools/orientationplot/README.txt. |
| * |
| * @hide |
| */ |
| public abstract class WindowOrientationListener { |
| private static final String TAG = "WindowOrientationListener"; |
| private static final boolean DEBUG = false; |
| private static final boolean localLOGV = DEBUG || false; |
| |
| private SensorManager mSensorManager; |
| private boolean mEnabled; |
| private int mRate; |
| private Sensor mSensor; |
| private SensorEventListenerImpl mSensorEventListener; |
| boolean mLogEnabled; |
| int mCurrentRotation = -1; |
| |
| /** |
| * Creates a new WindowOrientationListener. |
| * |
| * @param context for the WindowOrientationListener. |
| */ |
| public WindowOrientationListener(Context context) { |
| this(context, SensorManager.SENSOR_DELAY_UI); |
| } |
| |
| /** |
| * Creates a new WindowOrientationListener. |
| * |
| * @param context for the WindowOrientationListener. |
| * @param rate at which sensor events are processed (see also |
| * {@link android.hardware.SensorManager SensorManager}). Use the default |
| * value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL |
| * SENSOR_DELAY_NORMAL} for simple screen orientation change detection. |
| * |
| * This constructor is private since no one uses it. |
| */ |
| private WindowOrientationListener(Context context, int rate) { |
| mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); |
| mRate = rate; |
| mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); |
| if (mSensor != null) { |
| // Create listener only if sensors do exist |
| mSensorEventListener = new SensorEventListenerImpl(this); |
| } |
| } |
| |
| /** |
| * Enables the WindowOrientationListener so it will monitor the sensor and call |
| * {@link #onOrientationChanged} when the device orientation changes. |
| */ |
| public void enable() { |
| if (mSensor == null) { |
| Log.w(TAG, "Cannot detect sensors. Not enabled"); |
| return; |
| } |
| if (mEnabled == false) { |
| if (localLOGV) Log.d(TAG, "WindowOrientationListener enabled"); |
| mSensorManager.registerListener(mSensorEventListener, mSensor, mRate); |
| mEnabled = true; |
| } |
| } |
| |
| /** |
| * Disables the WindowOrientationListener. |
| */ |
| public void disable() { |
| if (mSensor == null) { |
| Log.w(TAG, "Cannot detect sensors. Invalid disable"); |
| return; |
| } |
| if (mEnabled == true) { |
| if (localLOGV) Log.d(TAG, "WindowOrientationListener disabled"); |
| mSensorManager.unregisterListener(mSensorEventListener); |
| mEnabled = false; |
| } |
| } |
| |
| /** |
| * Sets the current rotation. |
| * |
| * @param rotation The current rotation. |
| */ |
| public void setCurrentRotation(int rotation) { |
| mCurrentRotation = rotation; |
| } |
| |
| /** |
| * Gets the proposed rotation. |
| * |
| * This method only returns a rotation if the orientation listener is certain |
| * of its proposal. If the rotation is indeterminate, returns -1. |
| * |
| * @return The proposed rotation, or -1 if unknown. |
| */ |
| public int getProposedRotation() { |
| if (mEnabled) { |
| return mSensorEventListener.getProposedRotation(); |
| } |
| return -1; |
| } |
| |
| /** |
| * Returns true if sensor is enabled and false otherwise |
| */ |
| public boolean canDetectOrientation() { |
| return mSensor != null; |
| } |
| |
| /** |
| * Called when the rotation view of the device has changed. |
| * |
| * This method is called whenever the orientation becomes certain of an orientation. |
| * It is called each time the orientation determination transitions from being |
| * uncertain to being certain again, even if it is the same orientation as before. |
| * |
| * @param rotation The new orientation of the device, one of the Surface.ROTATION_* constants. |
| * @see Surface |
| */ |
| public abstract void onProposedRotationChanged(int rotation); |
| |
| /** |
| * Enables or disables the window orientation listener logging for use with |
| * the orientationplot.py tool. |
| * Logging is usually enabled via Development Settings. (See class comments.) |
| * @param enable True to enable logging. |
| */ |
| public void setLogEnabled(boolean enable) { |
| mLogEnabled = enable; |
| } |
| |
| /** |
| * 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: |
| * |
| * - Low-pass filter the accelerometer vector in cartesian coordinates. We do it in |
| * cartesian space because the orientation calculations are sensitive to the |
| * absolute magnitude of the acceleration. In particular, there are singularities |
| * in the calculation as the magnitude approaches 0. By performing the low-pass |
| * filtering early, we can eliminate high-frequency impulses systematically. |
| * |
| * - 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, 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. |
| * |
| * - If the tilt angle is too close to horizontal (near 90 or -90 degrees), do nothing. |
| * The orientation angle is not meaningful when the device is nearly horizontal. |
| * The tilt angle thresholds are set differently for each orientation and different |
| * limits are applied when the device is facing down as opposed to when it is facing |
| * forward or facing up. |
| * |
| * - When the orientation angle reaches a certain threshold, consider transitioning |
| * to the corresponding orientation. These thresholds have some hysteresis built-in |
| * to avoid oscillations between adjacent orientations. |
| * |
| * - Wait for the device to settle for a little bit. Once that happens, issue the |
| * new orientation proposal. |
| * |
| * Details are explained inline. |
| */ |
| static final class SensorEventListenerImpl implements SensorEventListener { |
| // We work with all angles in degrees in this class. |
| private static final float RADIANS_TO_DEGREES = (float) (180 / Math.PI); |
| |
| // Indices into SensorEvent.values for the accelerometer sensor. |
| private static final int ACCELEROMETER_DATA_X = 0; |
| private static final int ACCELEROMETER_DATA_Y = 1; |
| private static final int ACCELEROMETER_DATA_Z = 2; |
| |
| private final WindowOrientationListener mOrientationListener; |
| |
| /* State for first order low-pass filtering of accelerometer data. |
| * See http://en.wikipedia.org/wiki/Low-pass_filter#Discrete-time_realization for |
| * signal processing background. |
| */ |
| |
| private long mLastTimestamp = Long.MAX_VALUE; // in nanoseconds |
| private float mLastFilteredX, mLastFilteredY, mLastFilteredZ; |
| |
| // The current proposal. We wait for the proposal to be stable for a |
| // certain amount of time before accepting it. |
| // |
| // The basic idea is to ignore intermediate poses of the device while the |
| // user is picking up, putting down or turning the device. |
| private int mProposalRotation; |
| private long mProposalAgeMS; |
| |
| // A historical trace of tilt and orientation angles. Used to determine whether |
| // the device posture has settled down. |
| private static final int HISTORY_SIZE = 20; |
| private int mHistoryIndex; // index of most recent sample |
| private int mHistoryLength; // length of historical trace |
| private final long[] mHistoryTimestampMS = new long[HISTORY_SIZE]; |
| private final float[] mHistoryMagnitudes = new float[HISTORY_SIZE]; |
| private final int[] mHistoryTiltAngles = new int[HISTORY_SIZE]; |
| private final int[] mHistoryOrientationAngles = new int[HISTORY_SIZE]; |
| |
| // The maximum sample inter-arrival time in milliseconds. |
| // If the acceleration samples are further apart than this amount in time, we reset the |
| // state of the low-pass filter and orientation properties. This helps to handle |
| // boundary conditions when the device is turned on, wakes from suspend or there is |
| // a significant gap in samples. |
| private static final float MAX_FILTER_DELTA_TIME_MS = 1000; |
| |
| // The acceleration filter time constant. |
| // |
| // This time constant is used to tune the acceleration filter such that |
| // impulses and vibrational noise (think car dock) is suppressed before we |
| // try to calculate the tilt and orientation angles. |
| // |
| // The filter time constant is related to the filter cutoff frequency, which is the |
| // frequency at which signals are attenuated by 3dB (half the passband power). |
| // Each successive octave beyond this frequency is attenuated by an additional 6dB. |
| // |
| // Given a time constant t in seconds, the filter cutoff frequency Fc in Hertz |
| // is given by Fc = 1 / (2pi * t). |
| // |
| // The higher the time constant, the lower the cutoff frequency, so more noise |
| // will be suppressed. |
| // |
| // Filtering adds latency proportional the time constant (inversely proportional |
| // to the cutoff frequency) so we don't want to make the time constant too |
| // large or we can lose responsiveness. |
| private static final float FILTER_TIME_CONSTANT_MS = 100.0f; |
| |
| /* State for orientation detection. */ |
| |
| // Thresholds for minimum and maximum allowable deviation from gravity. |
| // |
| // If the device is undergoing external acceleration (being bumped, in a car |
| // that is turning around a corner or a plane taking off) then the magnitude |
| // may be substantially more or less than gravity. This can skew our orientation |
| // detection by making us think that up is pointed in a different direction. |
| // |
| // Conversely, if the device is in freefall, then there will be no gravity to |
| // measure at all. This is problematic because we cannot detect the orientation |
| // without gravity to tell us which way is up. A magnitude near 0 produces |
| // singularities in the tilt and orientation calculations. |
| // |
| // In both cases, we postpone choosing an orientation. |
| private static final float MIN_ACCELERATION_MAGNITUDE = |
| SensorManager.STANDARD_GRAVITY * 0.5f; |
| private static final float MAX_ACCELERATION_MAGNITUDE = |
| SensorManager.STANDARD_GRAVITY * 1.5f; |
| |
| // 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; |
| |
| // The tilt angle range in degrees for each orientation. |
| // Beyond these tilt angles, we don't even consider transitioning into the |
| // specified orientation. We place more stringent requirements on unnatural |
| // orientations than natural ones to make it less likely to accidentally transition |
| // into those states. |
| // The first value of each pair is negative so it applies a limit when the device is |
| // facing down (overhead reading in bed). |
| // The second value of each pair is positive so it applies a limit when the device is |
| // facing up (resting on a table). |
| // The ideal tilt angle is 0 (when the device is vertical) so the limits establish |
| // how close to vertical the device must be in order to change orientation. |
| private static final int[][] TILT_TOLERANCE = new int[][] { |
| /* ROTATION_0 */ { -20, 70 }, |
| /* ROTATION_90 */ { -20, 60 }, |
| /* ROTATION_180 */ { -20, 50 }, |
| /* ROTATION_270 */ { -20, 60 } |
| }; |
| |
| // The gap angle in degrees between adjacent orientation angles for hysteresis. |
| // This creates a "dead zone" between the current orientation and a proposed |
| // adjacent orientation. No orientation proposal is made when the orientation |
| // angle is within the gap between the current orientation and the adjacent |
| // orientation. |
| private static final int ADJACENT_ORIENTATION_ANGLE_GAP = 45; |
| |
| // The number of milliseconds for which the device posture must be stable |
| // before we perform an orientation change. If the device appears to be rotating |
| // (being picked up, put down) then we keep waiting until it settles. |
| private static final int SETTLE_TIME_MS = 200; |
| |
| // The maximum change in magnitude that can occur during the settle time. |
| // Tuning this constant particularly helps to filter out situations where the |
| // device is being picked up or put down by the user. |
| private static final float SETTLE_MAGNITUDE_MAX_DELTA = |
| SensorManager.STANDARD_GRAVITY * 0.2f; |
| |
| // The maximum change in tilt angle that can occur during the settle time. |
| private static final int SETTLE_TILT_ANGLE_MAX_DELTA = 5; |
| |
| // The maximum change in orientation angle that can occur during the settle time. |
| private static final int SETTLE_ORIENTATION_ANGLE_MAX_DELTA = 5; |
| |
| public SensorEventListenerImpl(WindowOrientationListener orientationListener) { |
| mOrientationListener = orientationListener; |
| } |
| |
| public int getProposedRotation() { |
| return mProposalAgeMS >= SETTLE_TIME_MS ? mProposalRotation : -1; |
| } |
| |
| @Override |
| public void onAccuracyChanged(Sensor sensor, int accuracy) { |
| } |
| |
| @Override |
| public void onSensorChanged(SensorEvent event) { |
| final boolean log = mOrientationListener.mLogEnabled; |
| |
| // The vector given in the SensorEvent points straight up (towards the sky) under ideal |
| // conditions (the phone is not accelerating). I'll call this up vector elsewhere. |
| float x = event.values[ACCELEROMETER_DATA_X]; |
| float y = event.values[ACCELEROMETER_DATA_Y]; |
| float z = event.values[ACCELEROMETER_DATA_Z]; |
| |
| if (log) { |
| Slog.v(TAG, "Raw acceleration vector: " + |
| "x=" + x + ", y=" + y + ", z=" + z); |
| } |
| |
| // Apply a low-pass filter to the acceleration up vector in cartesian space. |
| // Reset the orientation listener state if the samples are too far apart in time |
| // or when we see values of (0, 0, 0) which indicates that we polled the |
| // accelerometer too soon after turning it on and we don't have any data yet. |
| final long now = event.timestamp; |
| final float timeDeltaMS = (now - mLastTimestamp) * 0.000001f; |
| boolean skipSample; |
| if (timeDeltaMS <= 0 || timeDeltaMS > MAX_FILTER_DELTA_TIME_MS |
| || (x == 0 && y == 0 && z == 0)) { |
| if (log) { |
| Slog.v(TAG, "Resetting orientation listener."); |
| } |
| clearProposal(); |
| skipSample = true; |
| } else { |
| final float alpha = timeDeltaMS / (FILTER_TIME_CONSTANT_MS + timeDeltaMS); |
| x = alpha * (x - mLastFilteredX) + mLastFilteredX; |
| y = alpha * (y - mLastFilteredY) + mLastFilteredY; |
| z = alpha * (z - mLastFilteredZ) + mLastFilteredZ; |
| if (log) { |
| Slog.v(TAG, "Filtered acceleration vector: " + |
| "x=" + x + ", y=" + y + ", z=" + z); |
| } |
| skipSample = false; |
| } |
| mLastTimestamp = now; |
| mLastFilteredX = x; |
| mLastFilteredY = y; |
| mLastFilteredZ = z; |
| |
| final int oldProposedRotation = getProposedRotation(); |
| if (!skipSample) { |
| // Calculate the magnitude of the acceleration vector. |
| final float magnitude = (float) Math.sqrt(x * x + y * y + z * z); |
| if (magnitude < MIN_ACCELERATION_MAGNITUDE |
| || magnitude > MAX_ACCELERATION_MAGNITUDE) { |
| if (log) { |
| Slog.v(TAG, "Ignoring sensor data, magnitude out of range: " |
| + "magnitude=" + magnitude); |
| } |
| clearProposal(); |
| } else { |
| // Calculate the tilt angle. |
| // This is the angle between the up vector and the x-y plane (the plane of |
| // the screen) in a range of [-90, 90] degrees. |
| // -90 degrees: screen horizontal and facing the ground (overhead) |
| // 0 degrees: screen vertical |
| // 90 degrees: screen horizontal and facing the sky (on table) |
| final int tiltAngle = (int) Math.round( |
| Math.asin(z / magnitude) * RADIANS_TO_DEGREES); |
| |
| // If the tilt angle is too close to horizontal then we cannot determine |
| // the orientation angle of the screen. |
| if (Math.abs(tiltAngle) > MAX_TILT) { |
| if (log) { |
| Slog.v(TAG, "Ignoring sensor data, tilt angle too high: " |
| + "magnitude=" + magnitude + ", tiltAngle=" + tiltAngle); |
| } |
| clearProposal(); |
| } else { |
| // Calculate the orientation angle. |
| // This is the angle between the x-y projection of the up vector onto |
| // the +y-axis, increasing clockwise in a range of [0, 360] degrees. |
| int orientationAngle = (int) Math.round( |
| -Math.atan2(-x, y) * RADIANS_TO_DEGREES); |
| if (orientationAngle < 0) { |
| // atan2 returns [-180, 180]; normalize to [0, 360] |
| orientationAngle += 360; |
| } |
| |
| // Find the nearest rotation. |
| int nearestRotation = (orientationAngle + 45) / 90; |
| if (nearestRotation == 4) { |
| nearestRotation = 0; |
| } |
| |
| // Determine the proposed orientation. |
| // The confidence of the proposal is 1.0 when it is ideal and it |
| // decays exponentially as the proposal moves further from the ideal |
| // angle, tilt and magnitude of the proposed orientation. |
| if (!isTiltAngleAcceptable(nearestRotation, tiltAngle) |
| || !isOrientationAngleAcceptable(nearestRotation, |
| orientationAngle)) { |
| if (log) { |
| Slog.v(TAG, "Ignoring sensor data, no proposal: " |
| + "magnitude=" + magnitude + ", tiltAngle=" + tiltAngle |
| + ", orientationAngle=" + orientationAngle); |
| } |
| clearProposal(); |
| } else { |
| if (log) { |
| Slog.v(TAG, "Proposal: " |
| + "magnitude=" + magnitude |
| + ", tiltAngle=" + tiltAngle |
| + ", orientationAngle=" + orientationAngle |
| + ", proposalRotation=" + mProposalRotation); |
| } |
| updateProposal(nearestRotation, now / 1000000L, |
| magnitude, tiltAngle, orientationAngle); |
| } |
| } |
| } |
| } |
| |
| // Write final statistics about where we are in the orientation detection process. |
| final int proposedRotation = getProposedRotation(); |
| if (log) { |
| final float proposalConfidence = Math.min( |
| mProposalAgeMS * 1.0f / SETTLE_TIME_MS, 1.0f); |
| Slog.v(TAG, "Result: currentRotation=" + mOrientationListener.mCurrentRotation |
| + ", proposedRotation=" + proposedRotation |
| + ", timeDeltaMS=" + timeDeltaMS |
| + ", proposalRotation=" + mProposalRotation |
| + ", proposalAgeMS=" + mProposalAgeMS |
| + ", proposalConfidence=" + proposalConfidence); |
| } |
| |
| // Tell the listener. |
| if (proposedRotation != oldProposedRotation && proposedRotation >= 0) { |
| if (log) { |
| Slog.v(TAG, "Proposed rotation changed! proposedRotation=" + proposedRotation |
| + ", oldProposedRotation=" + oldProposedRotation); |
| } |
| mOrientationListener.onProposedRotationChanged(proposedRotation); |
| } |
| } |
| |
| /** |
| * Returns true if the tilt angle is acceptable for a proposed |
| * orientation transition. |
| */ |
| private boolean isTiltAngleAcceptable(int proposedRotation, |
| int tiltAngle) { |
| return tiltAngle >= TILT_TOLERANCE[proposedRotation][0] |
| && tiltAngle <= TILT_TOLERANCE[proposedRotation][1]; |
| } |
| |
| /** |
| * Returns true if the orientation angle is acceptable for a proposed |
| * orientation transition. |
| * |
| * This function takes into account the gap between adjacent orientations |
| * for hysteresis. |
| */ |
| private boolean isOrientationAngleAcceptable(int proposedRotation, int orientationAngle) { |
| // If there is no current rotation, then there is no gap. |
| // The gap is used only to introduce hysteresis among advertised orientation |
| // changes to avoid flapping. |
| final int currentRotation = mOrientationListener.mCurrentRotation; |
| if (currentRotation >= 0) { |
| // If the proposed rotation is the same or is counter-clockwise adjacent, |
| // then we set a lower bound on the orientation angle. |
| // For example, if currentRotation is ROTATION_0 and proposed is ROTATION_90, |
| // then we want to check orientationAngle > 45 + GAP / 2. |
| if (proposedRotation == currentRotation |
| || proposedRotation == (currentRotation + 1) % 4) { |
| int lowerBound = proposedRotation * 90 - 45 |
| + ADJACENT_ORIENTATION_ANGLE_GAP / 2; |
| if (proposedRotation == 0) { |
| if (orientationAngle >= 315 && orientationAngle < lowerBound + 360) { |
| return false; |
| } |
| } else { |
| if (orientationAngle < lowerBound) { |
| return false; |
| } |
| } |
| } |
| |
| // If the proposed rotation is the same or is clockwise adjacent, |
| // then we set an upper bound on the orientation angle. |
| // For example, if currentRotation is ROTATION_0 and proposed is ROTATION_270, |
| // then we want to check orientationAngle < 315 - GAP / 2. |
| if (proposedRotation == currentRotation |
| || proposedRotation == (currentRotation + 3) % 4) { |
| int upperBound = proposedRotation * 90 + 45 |
| - ADJACENT_ORIENTATION_ANGLE_GAP / 2; |
| if (proposedRotation == 0) { |
| if (orientationAngle <= 45 && orientationAngle > upperBound) { |
| return false; |
| } |
| } else { |
| if (orientationAngle > upperBound) { |
| return false; |
| } |
| } |
| } |
| } |
| return true; |
| } |
| |
| private void clearProposal() { |
| mProposalRotation = -1; |
| mProposalAgeMS = 0; |
| } |
| |
| private void updateProposal(int rotation, long timestampMS, |
| float magnitude, int tiltAngle, int orientationAngle) { |
| if (mProposalRotation != rotation) { |
| mProposalRotation = rotation; |
| mHistoryIndex = 0; |
| mHistoryLength = 0; |
| } |
| |
| final int index = mHistoryIndex; |
| mHistoryTimestampMS[index] = timestampMS; |
| mHistoryMagnitudes[index] = magnitude; |
| mHistoryTiltAngles[index] = tiltAngle; |
| mHistoryOrientationAngles[index] = orientationAngle; |
| mHistoryIndex = (index + 1) % HISTORY_SIZE; |
| if (mHistoryLength < HISTORY_SIZE) { |
| mHistoryLength += 1; |
| } |
| |
| long age = 0; |
| for (int i = 1; i < mHistoryLength; i++) { |
| final int olderIndex = (index + HISTORY_SIZE - i) % HISTORY_SIZE; |
| if (Math.abs(mHistoryMagnitudes[olderIndex] - magnitude) |
| > SETTLE_MAGNITUDE_MAX_DELTA) { |
| break; |
| } |
| if (angleAbsoluteDelta(mHistoryTiltAngles[olderIndex], |
| tiltAngle) > SETTLE_TILT_ANGLE_MAX_DELTA) { |
| break; |
| } |
| if (angleAbsoluteDelta(mHistoryOrientationAngles[olderIndex], |
| orientationAngle) > SETTLE_ORIENTATION_ANGLE_MAX_DELTA) { |
| break; |
| } |
| age = timestampMS - mHistoryTimestampMS[olderIndex]; |
| if (age >= SETTLE_TIME_MS) { |
| break; |
| } |
| } |
| mProposalAgeMS = age; |
| } |
| |
| private static int angleAbsoluteDelta(int a, int b) { |
| int delta = Math.abs(a - b); |
| if (delta > 180) { |
| delta = 360 - delta; |
| } |
| return delta; |
| } |
| } |
| } |