| /* |
| * 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 com.android.server.policy; |
| |
| import static com.android.server.wm.WindowOrientationListenerProto.ENABLED; |
| import static com.android.server.wm.WindowOrientationListenerProto.ROTATION; |
| |
| import android.content.Context; |
| import android.hardware.Sensor; |
| import android.hardware.SensorEvent; |
| import android.hardware.SensorEventListener; |
| import android.hardware.SensorManager; |
| import android.os.Handler; |
| import android.os.SystemClock; |
| import android.os.SystemProperties; |
| import android.util.Slog; |
| import android.util.proto.ProtoOutputStream; |
| import android.view.Surface; |
| |
| import java.io.PrintWriter; |
| import java.util.List; |
| |
| /** |
| * 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. |
| * Refer to frameworks/base/tools/orientationplot/README.txt for details. |
| */ |
| public abstract class WindowOrientationListener { |
| private static final String TAG = "WindowOrientationListener"; |
| private static final boolean LOG = SystemProperties.getBoolean( |
| "debug.orientation.log", false); |
| |
| private static final boolean USE_GRAVITY_SENSOR = false; |
| private static final int DEFAULT_BATCH_LATENCY = 100000; |
| |
| private Handler mHandler; |
| private SensorManager mSensorManager; |
| private boolean mEnabled; |
| private int mRate; |
| private String mSensorType; |
| private Sensor mSensor; |
| private OrientationJudge mOrientationJudge; |
| private int mCurrentRotation = -1; |
| |
| private final Object mLock = new Object(); |
| |
| /** |
| * Creates a new WindowOrientationListener. |
| * |
| * @param context for the WindowOrientationListener. |
| * @param handler Provides the Looper for receiving sensor updates. |
| */ |
| public WindowOrientationListener(Context context, Handler handler) { |
| this(context, handler, SensorManager.SENSOR_DELAY_UI); |
| } |
| |
| /** |
| * Creates a new WindowOrientationListener. |
| * |
| * @param context for the WindowOrientationListener. |
| * @param handler Provides the Looper for receiving sensor updates. |
| * @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, Handler handler, int rate) { |
| mHandler = handler; |
| mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); |
| mRate = rate; |
| List<Sensor> l = mSensorManager.getSensorList(Sensor.TYPE_DEVICE_ORIENTATION); |
| Sensor wakeUpDeviceOrientationSensor = null; |
| Sensor nonWakeUpDeviceOrientationSensor = null; |
| /** |
| * Prefer the wakeup form of the sensor if implemented. |
| * It's OK to look for just two types of this sensor and use |
| * the last found. Typical devices will only have one sensor of |
| * this type. |
| */ |
| for (Sensor s : l) { |
| if (s.isWakeUpSensor()) { |
| wakeUpDeviceOrientationSensor = s; |
| } else { |
| nonWakeUpDeviceOrientationSensor = s; |
| } |
| } |
| |
| if (wakeUpDeviceOrientationSensor != null) { |
| mSensor = wakeUpDeviceOrientationSensor; |
| } else { |
| mSensor = nonWakeUpDeviceOrientationSensor; |
| } |
| |
| if (mSensor != null) { |
| mOrientationJudge = new OrientationSensorJudge(); |
| } |
| |
| if (mOrientationJudge == null) { |
| mSensor = mSensorManager.getDefaultSensor(USE_GRAVITY_SENSOR |
| ? Sensor.TYPE_GRAVITY : Sensor.TYPE_ACCELEROMETER); |
| if (mSensor != null) { |
| // Create listener only if sensors do exist |
| mOrientationJudge = new AccelSensorJudge(context); |
| } |
| } |
| } |
| |
| /** |
| * Enables the WindowOrientationListener so it will monitor the sensor and call |
| * {@link #onProposedRotationChanged(int)} when the device orientation changes. |
| */ |
| public void enable() { |
| enable(true /* clearCurrentRotation */); |
| } |
| |
| /** |
| * Enables the WindowOrientationListener so it will monitor the sensor and call |
| * {@link #onProposedRotationChanged(int)} when the device orientation changes. |
| * |
| * @param clearCurrentRotation True if the current proposed sensor rotation should be cleared as |
| * part of the reset. |
| */ |
| public void enable(boolean clearCurrentRotation) { |
| synchronized (mLock) { |
| if (mSensor == null) { |
| Slog.w(TAG, "Cannot detect sensors. Not enabled"); |
| return; |
| } |
| if (mEnabled) { |
| return; |
| } |
| if (LOG) { |
| Slog.d(TAG, "WindowOrientationListener enabled clearCurrentRotation=" |
| + clearCurrentRotation); |
| } |
| mOrientationJudge.resetLocked(clearCurrentRotation); |
| if (mSensor.getType() == Sensor.TYPE_ACCELEROMETER) { |
| mSensorManager.registerListener( |
| mOrientationJudge, mSensor, mRate, DEFAULT_BATCH_LATENCY, mHandler); |
| } else { |
| mSensorManager.registerListener(mOrientationJudge, mSensor, mRate, mHandler); |
| } |
| mEnabled = true; |
| } |
| } |
| |
| /** |
| * Disables the WindowOrientationListener. |
| */ |
| public void disable() { |
| synchronized (mLock) { |
| if (mSensor == null) { |
| Slog.w(TAG, "Cannot detect sensors. Invalid disable"); |
| return; |
| } |
| if (mEnabled == true) { |
| if (LOG) { |
| Slog.d(TAG, "WindowOrientationListener disabled"); |
| } |
| mSensorManager.unregisterListener(mOrientationJudge); |
| mEnabled = false; |
| } |
| } |
| } |
| |
| public void onTouchStart() { |
| synchronized (mLock) { |
| if (mOrientationJudge != null) { |
| mOrientationJudge.onTouchStartLocked(); |
| } |
| } |
| } |
| |
| public void onTouchEnd() { |
| long whenElapsedNanos = SystemClock.elapsedRealtimeNanos(); |
| |
| synchronized (mLock) { |
| if (mOrientationJudge != null) { |
| mOrientationJudge.onTouchEndLocked(whenElapsedNanos); |
| } |
| } |
| } |
| |
| public Handler getHandler() { |
| return mHandler; |
| } |
| |
| /** |
| * Sets the current rotation. |
| * |
| * @param rotation The current rotation. |
| */ |
| public void setCurrentRotation(int rotation) { |
| synchronized (mLock) { |
| 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() { |
| synchronized (mLock) { |
| if (mEnabled) { |
| return mOrientationJudge.getProposedRotationLocked(); |
| } |
| return -1; |
| } |
| } |
| |
| /** |
| * Returns true if sensor is enabled and false otherwise |
| */ |
| public boolean canDetectOrientation() { |
| synchronized (mLock) { |
| 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. |
| * |
| * This should only be called on the Handler thread. |
| * |
| * @param rotation The new orientation of the device, one of the Surface.ROTATION_* constants. |
| * @see android.view.Surface |
| */ |
| public abstract void onProposedRotationChanged(int rotation); |
| |
| public void dumpDebug(ProtoOutputStream proto, long fieldId) { |
| final long token = proto.start(fieldId); |
| synchronized (mLock) { |
| proto.write(ENABLED, mEnabled); |
| proto.write(ROTATION, mCurrentRotation); |
| } |
| proto.end(token); |
| } |
| |
| public void dump(PrintWriter pw, String prefix) { |
| synchronized (mLock) { |
| pw.println(prefix + TAG); |
| prefix += " "; |
| pw.println(prefix + "mEnabled=" + mEnabled); |
| pw.println(prefix + "mCurrentRotation=" + Surface.rotationToString(mCurrentRotation)); |
| pw.println(prefix + "mSensorType=" + mSensorType); |
| pw.println(prefix + "mSensor=" + mSensor); |
| pw.println(prefix + "mRate=" + mRate); |
| |
| if (mOrientationJudge != null) { |
| mOrientationJudge.dumpLocked(pw, prefix); |
| } |
| } |
| } |
| |
| abstract class OrientationJudge implements SensorEventListener { |
| // Number of nanoseconds per millisecond. |
| protected static final long NANOS_PER_MS = 1000000; |
| |
| // Number of milliseconds per nano second. |
| protected static final float MILLIS_PER_NANO = 0.000001f; |
| |
| // The minimum amount of time that must have elapsed since the screen was last touched |
| // before the proposed rotation can change. |
| protected static final long PROPOSAL_MIN_TIME_SINCE_TOUCH_END_NANOS = |
| 500 * NANOS_PER_MS; |
| |
| /** |
| * 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. |
| * |
| * Should only be called when holding WindowOrientationListener lock. |
| * |
| * @return The proposed rotation, or -1 if unknown. |
| */ |
| public abstract int getProposedRotationLocked(); |
| |
| /** |
| * Notifies the orientation judge that the screen is being touched. |
| * |
| * Should only be called when holding WindowOrientationListener lock. |
| */ |
| public abstract void onTouchStartLocked(); |
| |
| /** |
| * Notifies the orientation judge that the screen is no longer being touched. |
| * |
| * Should only be called when holding WindowOrientationListener lock. |
| * |
| * @param whenElapsedNanos Given in the elapsed realtime nanos time base. |
| */ |
| public abstract void onTouchEndLocked(long whenElapsedNanos); |
| |
| /** |
| * Resets the state of the judge. |
| * |
| * Should only be called when holding WindowOrientationListener lock. |
| * |
| * @param clearCurrentRotation True if the current proposed sensor rotation should be |
| * cleared as part of the reset. |
| */ |
| public abstract void resetLocked(boolean clearCurrentRotation); |
| |
| /** |
| * Dumps internal state of the orientation judge. |
| * |
| * Should only be called when holding WindowOrientationListener lock. |
| */ |
| public abstract void dumpLocked(PrintWriter pw, String prefix); |
| |
| @Override |
| public abstract void onAccuracyChanged(Sensor sensor, int accuracy); |
| |
| @Override |
| public abstract void onSensorChanged(SensorEvent event); |
| } |
| |
| /** |
| * 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 most spurious high-frequency impulses due to noise. |
| * |
| * - 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. |
| * |
| * See http://en.wikipedia.org/wiki/Low-pass_filter#Discrete-time_realization for |
| * signal processing background. |
| */ |
| final class AccelSensorJudge extends OrientationJudge { |
| // 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; |
| |
| // The minimum amount of time that a predicted rotation must be stable before it |
| // is accepted as a valid rotation proposal. This value can be quite small because |
| // the low-pass filter already suppresses most of the noise so we're really just |
| // looking for quick confirmation that the last few samples are in agreement as to |
| // the desired orientation. |
| private static final long PROPOSAL_SETTLE_TIME_NANOS = 40 * NANOS_PER_MS; |
| |
| // The minimum amount of time that must have elapsed since the device last exited |
| // the flat state (time since it was picked up) before the proposed rotation |
| // can change. |
| private static final long PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS = 500 * NANOS_PER_MS; |
| |
| // The minimum amount of time that must have elapsed since the device stopped |
| // swinging (time since device appeared to be in the process of being put down |
| // or put away into a pocket) before the proposed rotation can change. |
| private static final long PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS = 300 * NANOS_PER_MS; |
| |
| // The minimum amount of time that must have elapsed since the device stopped |
| // undergoing external acceleration before the proposed rotation can change. |
| private static final long PROPOSAL_MIN_TIME_SINCE_ACCELERATION_ENDED_NANOS = |
| 500 * NANOS_PER_MS; |
| |
| // If the tilt angle remains greater than the specified angle for a minimum of |
| // the specified time, then the device is deemed to be lying flat |
| // (just chillin' on a table). |
| private static final float FLAT_ANGLE = 80; |
| private static final long FLAT_TIME_NANOS = 1000 * NANOS_PER_MS; |
| |
| // If the tilt angle has increased by at least delta degrees within the specified amount |
| // of time, then the device is deemed to be swinging away from the user |
| // down towards flat (tilt = 90). |
| private static final float SWING_AWAY_ANGLE_DELTA = 20; |
| private static final long SWING_TIME_NANOS = 300 * NANOS_PER_MS; |
| |
| // 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 long MAX_FILTER_DELTA_TIME_NANOS = 1000 * NANOS_PER_MS; |
| |
| // 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. Likewise we don't want to make it too |
| // small or we do a poor job suppressing acceleration spikes. |
| // Empirically, 100ms seems to be too small and 500ms is too large. |
| private static final float FILTER_TIME_CONSTANT_MS = 200.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. |
| // |
| // However, we need to tolerate some acceleration because the angular momentum |
| // of turning the device can skew the observed acceleration for a short period of time. |
| private static final float NEAR_ZERO_MAGNITUDE = 1; // m/s^2 |
| private static final float ACCELERATION_TOLERANCE = 4; // m/s^2 |
| private static final float MIN_ACCELERATION_MAGNITUDE = |
| SensorManager.STANDARD_GRAVITY - ACCELERATION_TOLERANCE; |
| private static final float MAX_ACCELERATION_MAGNITUDE = |
| SensorManager.STANDARD_GRAVITY + ACCELERATION_TOLERANCE; |
| |
| // 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 |
| // because it's too unstable. |
| private static final int MAX_TILT = 80; |
| |
| // The tilt angle below which we conclude that the user is holding the device |
| // overhead reading in bed and lock into that state. |
| private static final int TILT_OVERHEAD_ENTER = -40; |
| |
| // The tilt angle above which we conclude that the user would like a rotation |
| // change to occur and unlock from the overhead state. |
| private static final int TILT_OVERHEAD_EXIT = -15; |
| |
| // 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 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 final int[][] mTiltToleranceConfig = new int[][] { |
| /* ROTATION_0 */ { -25, 70 }, // note: these are overridden by config.xml |
| /* ROTATION_90 */ { -25, 65 }, |
| /* ROTATION_180 */ { -25, 60 }, |
| /* ROTATION_270 */ { -25, 65 } |
| }; |
| |
| // Timestamp and value of the last accelerometer sample. |
| private long mLastFilteredTimestampNanos; |
| private float mLastFilteredX, mLastFilteredY, mLastFilteredZ; |
| |
| // The last proposed rotation, -1 if unknown. |
| private int mProposedRotation; |
| |
| // Value of the current predicted rotation, -1 if unknown. |
| private int mPredictedRotation; |
| |
| // Timestamp of when the predicted rotation most recently changed. |
| private long mPredictedRotationTimestampNanos; |
| |
| // Timestamp when the device last appeared to be flat for sure (the flat delay elapsed). |
| private long mFlatTimestampNanos; |
| private boolean mFlat; |
| |
| // Timestamp when the device last appeared to be swinging. |
| private long mSwingTimestampNanos; |
| private boolean mSwinging; |
| |
| // Timestamp when the device last appeared to be undergoing external acceleration. |
| private long mAccelerationTimestampNanos; |
| private boolean mAccelerating; |
| |
| // Timestamp when the last touch to the touch screen ended |
| private long mTouchEndedTimestampNanos = Long.MIN_VALUE; |
| private boolean mTouched; |
| |
| // Whether we are locked into an overhead usage mode. |
| private boolean mOverhead; |
| |
| // History of observed tilt angles. |
| private static final int TILT_HISTORY_SIZE = 200; |
| private float[] mTiltHistory = new float[TILT_HISTORY_SIZE]; |
| private long[] mTiltHistoryTimestampNanos = new long[TILT_HISTORY_SIZE]; |
| private int mTiltHistoryIndex; |
| |
| public AccelSensorJudge(Context context) { |
| // Load tilt tolerance configuration. |
| int[] tiltTolerance = context.getResources().getIntArray( |
| com.android.internal.R.array.config_autoRotationTiltTolerance); |
| if (tiltTolerance.length == 8) { |
| for (int i = 0; i < 4; i++) { |
| int min = tiltTolerance[i * 2]; |
| int max = tiltTolerance[i * 2 + 1]; |
| if (min >= -90 && min <= max && max <= 90) { |
| mTiltToleranceConfig[i][0] = min; |
| mTiltToleranceConfig[i][1] = max; |
| } else { |
| Slog.wtf(TAG, "config_autoRotationTiltTolerance contains invalid range: " |
| + "min=" + min + ", max=" + max); |
| } |
| } |
| } else { |
| Slog.wtf(TAG, "config_autoRotationTiltTolerance should have exactly 8 elements"); |
| } |
| } |
| |
| @Override |
| public int getProposedRotationLocked() { |
| return mProposedRotation; |
| } |
| |
| @Override |
| public void dumpLocked(PrintWriter pw, String prefix) { |
| pw.println(prefix + "AccelSensorJudge"); |
| prefix += " "; |
| pw.println(prefix + "mProposedRotation=" + mProposedRotation); |
| pw.println(prefix + "mPredictedRotation=" + mPredictedRotation); |
| pw.println(prefix + "mLastFilteredX=" + mLastFilteredX); |
| pw.println(prefix + "mLastFilteredY=" + mLastFilteredY); |
| pw.println(prefix + "mLastFilteredZ=" + mLastFilteredZ); |
| final long delta = SystemClock.elapsedRealtimeNanos() - mLastFilteredTimestampNanos; |
| pw.println(prefix + "mLastFilteredTimestampNanos=" + mLastFilteredTimestampNanos |
| + " (" + (delta * 0.000001f) + "ms ago)"); |
| pw.println(prefix + "mTiltHistory={last: " + getLastTiltLocked() + "}"); |
| pw.println(prefix + "mFlat=" + mFlat); |
| pw.println(prefix + "mSwinging=" + mSwinging); |
| pw.println(prefix + "mAccelerating=" + mAccelerating); |
| pw.println(prefix + "mOverhead=" + mOverhead); |
| pw.println(prefix + "mTouched=" + mTouched); |
| pw.print(prefix + "mTiltToleranceConfig=["); |
| for (int i = 0; i < 4; i++) { |
| if (i != 0) { |
| pw.print(", "); |
| } |
| pw.print("["); |
| pw.print(mTiltToleranceConfig[i][0]); |
| pw.print(", "); |
| pw.print(mTiltToleranceConfig[i][1]); |
| pw.print("]"); |
| } |
| pw.println("]"); |
| } |
| |
| @Override |
| public void onAccuracyChanged(Sensor sensor, int accuracy) { |
| } |
| |
| @Override |
| public void onSensorChanged(SensorEvent event) { |
| int proposedRotation; |
| int oldProposedRotation; |
| |
| synchronized (mLock) { |
| // 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 |
| + ", magnitude=" + Math.sqrt(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 long then = mLastFilteredTimestampNanos; |
| final float timeDeltaMS = (now - then) * 0.000001f; |
| final boolean skipSample; |
| if (now < then |
| || now > then + MAX_FILTER_DELTA_TIME_NANOS |
| || (x == 0 && y == 0 && z == 0)) { |
| if (LOG) { |
| Slog.v(TAG, "Resetting orientation listener."); |
| } |
| resetLocked(true /* clearCurrentRotation */); |
| 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 |
| + ", magnitude=" + Math.sqrt(x * x + y * y + z * z)); |
| } |
| skipSample = false; |
| } |
| mLastFilteredTimestampNanos = now; |
| mLastFilteredX = x; |
| mLastFilteredY = y; |
| mLastFilteredZ = z; |
| |
| boolean isAccelerating = false; |
| boolean isFlat = false; |
| boolean isSwinging = false; |
| if (!skipSample) { |
| // Calculate the magnitude of the acceleration vector. |
| final float magnitude = (float) Math.sqrt(x * x + y * y + z * z); |
| if (magnitude < NEAR_ZERO_MAGNITUDE) { |
| if (LOG) { |
| Slog.v(TAG, "Ignoring sensor data, magnitude too close to zero."); |
| } |
| clearPredictedRotationLocked(); |
| } else { |
| // Determine whether the device appears to be undergoing external |
| // acceleration. |
| if (isAcceleratingLocked(magnitude)) { |
| isAccelerating = true; |
| mAccelerationTimestampNanos = now; |
| } |
| |
| // 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); |
| addTiltHistoryEntryLocked(now, tiltAngle); |
| |
| // Determine whether the device appears to be flat or swinging. |
| if (isFlatLocked(now)) { |
| isFlat = true; |
| mFlatTimestampNanos = now; |
| } |
| if (isSwingingLocked(now, tiltAngle)) { |
| isSwinging = true; |
| mSwingTimestampNanos = now; |
| } |
| |
| // If the tilt angle is too close to horizontal then we cannot determine |
| // the orientation angle of the screen. |
| if (tiltAngle <= TILT_OVERHEAD_ENTER) { |
| mOverhead = true; |
| } else if (tiltAngle >= TILT_OVERHEAD_EXIT) { |
| mOverhead = false; |
| } |
| if (mOverhead) { |
| if (LOG) { |
| Slog.v(TAG, "Ignoring sensor data, device is overhead: " |
| + "tiltAngle=" + tiltAngle); |
| } |
| clearPredictedRotationLocked(); |
| } else if (Math.abs(tiltAngle) > MAX_TILT) { |
| if (LOG) { |
| Slog.v(TAG, "Ignoring sensor data, tilt angle too high: " |
| + "tiltAngle=" + tiltAngle); |
| } |
| clearPredictedRotationLocked(); |
| } 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 predicted orientation. |
| if (isTiltAngleAcceptableLocked(nearestRotation, tiltAngle) |
| && isOrientationAngleAcceptableLocked(nearestRotation, |
| orientationAngle)) { |
| updatePredictedRotationLocked(now, nearestRotation); |
| if (LOG) { |
| Slog.v(TAG, "Predicted: " |
| + "tiltAngle=" + tiltAngle |
| + ", orientationAngle=" + orientationAngle |
| + ", predictedRotation=" + mPredictedRotation |
| + ", predictedRotationAgeMS=" |
| + ((now - mPredictedRotationTimestampNanos) |
| * 0.000001f)); |
| } |
| } else { |
| if (LOG) { |
| Slog.v(TAG, "Ignoring sensor data, no predicted rotation: " |
| + "tiltAngle=" + tiltAngle |
| + ", orientationAngle=" + orientationAngle); |
| } |
| clearPredictedRotationLocked(); |
| } |
| } |
| } |
| } |
| mFlat = isFlat; |
| mSwinging = isSwinging; |
| mAccelerating = isAccelerating; |
| |
| // Determine new proposed rotation. |
| oldProposedRotation = mProposedRotation; |
| if (mPredictedRotation < 0 || isPredictedRotationAcceptableLocked(now)) { |
| mProposedRotation = mPredictedRotation; |
| } |
| proposedRotation = mProposedRotation; |
| |
| // Write final statistics about where we are in the orientation detection process. |
| if (LOG) { |
| Slog.v(TAG, "Result: currentRotation=" + mCurrentRotation |
| + ", proposedRotation=" + proposedRotation |
| + ", predictedRotation=" + mPredictedRotation |
| + ", timeDeltaMS=" + timeDeltaMS |
| + ", isAccelerating=" + isAccelerating |
| + ", isFlat=" + isFlat |
| + ", isSwinging=" + isSwinging |
| + ", isOverhead=" + mOverhead |
| + ", isTouched=" + mTouched |
| + ", timeUntilSettledMS=" + remainingMS(now, |
| mPredictedRotationTimestampNanos + PROPOSAL_SETTLE_TIME_NANOS) |
| + ", timeUntilAccelerationDelayExpiredMS=" + remainingMS(now, |
| mAccelerationTimestampNanos + PROPOSAL_MIN_TIME_SINCE_ACCELERATION_ENDED_NANOS) |
| + ", timeUntilFlatDelayExpiredMS=" + remainingMS(now, |
| mFlatTimestampNanos + PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS) |
| + ", timeUntilSwingDelayExpiredMS=" + remainingMS(now, |
| mSwingTimestampNanos + PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS) |
| + ", timeUntilTouchDelayExpiredMS=" + remainingMS(now, |
| mTouchEndedTimestampNanos + PROPOSAL_MIN_TIME_SINCE_TOUCH_END_NANOS)); |
| } |
| } |
| |
| // Tell the listener. |
| if (proposedRotation != oldProposedRotation && proposedRotation >= 0) { |
| if (LOG) { |
| Slog.v(TAG, "Proposed rotation changed! proposedRotation=" + proposedRotation |
| + ", oldProposedRotation=" + oldProposedRotation); |
| } |
| onProposedRotationChanged(proposedRotation); |
| } |
| } |
| |
| @Override |
| public void onTouchStartLocked() { |
| mTouched = true; |
| } |
| |
| @Override |
| public void onTouchEndLocked(long whenElapsedNanos) { |
| mTouched = false; |
| mTouchEndedTimestampNanos = whenElapsedNanos; |
| } |
| |
| @Override |
| public void resetLocked(boolean clearCurrentRotation) { |
| mLastFilteredTimestampNanos = Long.MIN_VALUE; |
| if (clearCurrentRotation) { |
| mProposedRotation = -1; |
| } |
| mFlatTimestampNanos = Long.MIN_VALUE; |
| mFlat = false; |
| mSwingTimestampNanos = Long.MIN_VALUE; |
| mSwinging = false; |
| mAccelerationTimestampNanos = Long.MIN_VALUE; |
| mAccelerating = false; |
| mOverhead = false; |
| clearPredictedRotationLocked(); |
| clearTiltHistoryLocked(); |
| } |
| |
| |
| /** |
| * Returns true if the tilt angle is acceptable for a given predicted rotation. |
| */ |
| private boolean isTiltAngleAcceptableLocked(int rotation, int tiltAngle) { |
| return tiltAngle >= mTiltToleranceConfig[rotation][0] |
| && tiltAngle <= mTiltToleranceConfig[rotation][1]; |
| } |
| |
| /** |
| * Returns true if the orientation angle is acceptable for a given predicted rotation. |
| * |
| * This function takes into account the gap between adjacent orientations |
| * for hysteresis. |
| */ |
| private boolean isOrientationAngleAcceptableLocked(int rotation, 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 = mCurrentRotation; |
| if (currentRotation >= 0) { |
| // If the specified rotation is the same or is counter-clockwise adjacent |
| // to the current rotation, 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 (rotation == currentRotation |
| || rotation == (currentRotation + 1) % 4) { |
| int lowerBound = rotation * 90 - 45 |
| + ADJACENT_ORIENTATION_ANGLE_GAP / 2; |
| if (rotation == 0) { |
| if (orientationAngle >= 315 && orientationAngle < lowerBound + 360) { |
| return false; |
| } |
| } else { |
| if (orientationAngle < lowerBound) { |
| return false; |
| } |
| } |
| } |
| |
| // If the specified 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 rotation is ROTATION_270, |
| // then we want to check orientationAngle < 315 - GAP / 2. |
| if (rotation == currentRotation |
| || rotation == (currentRotation + 3) % 4) { |
| int upperBound = rotation * 90 + 45 |
| - ADJACENT_ORIENTATION_ANGLE_GAP / 2; |
| if (rotation == 0) { |
| if (orientationAngle <= 45 && orientationAngle > upperBound) { |
| return false; |
| } |
| } else { |
| if (orientationAngle > upperBound) { |
| return false; |
| } |
| } |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Returns true if the predicted rotation is ready to be advertised as a |
| * proposed rotation. |
| */ |
| private boolean isPredictedRotationAcceptableLocked(long now) { |
| // The predicted rotation must have settled long enough. |
| if (now < mPredictedRotationTimestampNanos + PROPOSAL_SETTLE_TIME_NANOS) { |
| return false; |
| } |
| |
| // The last flat state (time since picked up) must have been sufficiently long ago. |
| if (now < mFlatTimestampNanos + PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS) { |
| return false; |
| } |
| |
| // The last swing state (time since last movement to put down) must have been |
| // sufficiently long ago. |
| if (now < mSwingTimestampNanos + PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS) { |
| return false; |
| } |
| |
| // The last acceleration state must have been sufficiently long ago. |
| if (now < mAccelerationTimestampNanos |
| + PROPOSAL_MIN_TIME_SINCE_ACCELERATION_ENDED_NANOS) { |
| return false; |
| } |
| |
| // The last touch must have ended sufficiently long ago. |
| if (mTouched || now < mTouchEndedTimestampNanos |
| + PROPOSAL_MIN_TIME_SINCE_TOUCH_END_NANOS) { |
| return false; |
| } |
| |
| // Looks good! |
| return true; |
| } |
| |
| private void clearPredictedRotationLocked() { |
| mPredictedRotation = -1; |
| mPredictedRotationTimestampNanos = Long.MIN_VALUE; |
| } |
| |
| private void updatePredictedRotationLocked(long now, int rotation) { |
| if (mPredictedRotation != rotation) { |
| mPredictedRotation = rotation; |
| mPredictedRotationTimestampNanos = now; |
| } |
| } |
| |
| private boolean isAcceleratingLocked(float magnitude) { |
| return magnitude < MIN_ACCELERATION_MAGNITUDE |
| || magnitude > MAX_ACCELERATION_MAGNITUDE; |
| } |
| |
| private void clearTiltHistoryLocked() { |
| mTiltHistoryTimestampNanos[0] = Long.MIN_VALUE; |
| mTiltHistoryIndex = 1; |
| } |
| |
| private void addTiltHistoryEntryLocked(long now, float tilt) { |
| mTiltHistory[mTiltHistoryIndex] = tilt; |
| mTiltHistoryTimestampNanos[mTiltHistoryIndex] = now; |
| mTiltHistoryIndex = (mTiltHistoryIndex + 1) % TILT_HISTORY_SIZE; |
| mTiltHistoryTimestampNanos[mTiltHistoryIndex] = Long.MIN_VALUE; |
| } |
| |
| private boolean isFlatLocked(long now) { |
| for (int i = mTiltHistoryIndex; (i = nextTiltHistoryIndexLocked(i)) >= 0; ) { |
| if (mTiltHistory[i] < FLAT_ANGLE) { |
| break; |
| } |
| if (mTiltHistoryTimestampNanos[i] + FLAT_TIME_NANOS <= now) { |
| // Tilt has remained greater than FLAT_TILT_ANGLE for FLAT_TIME_NANOS. |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean isSwingingLocked(long now, float tilt) { |
| for (int i = mTiltHistoryIndex; (i = nextTiltHistoryIndexLocked(i)) >= 0; ) { |
| if (mTiltHistoryTimestampNanos[i] + SWING_TIME_NANOS < now) { |
| break; |
| } |
| if (mTiltHistory[i] + SWING_AWAY_ANGLE_DELTA <= tilt) { |
| // Tilted away by SWING_AWAY_ANGLE_DELTA within SWING_TIME_NANOS. |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private int nextTiltHistoryIndexLocked(int index) { |
| index = (index == 0 ? TILT_HISTORY_SIZE : index) - 1; |
| return mTiltHistoryTimestampNanos[index] != Long.MIN_VALUE ? index : -1; |
| } |
| |
| private float getLastTiltLocked() { |
| int index = nextTiltHistoryIndexLocked(mTiltHistoryIndex); |
| return index >= 0 ? mTiltHistory[index] : Float.NaN; |
| } |
| |
| private float remainingMS(long now, long until) { |
| return now >= until ? 0 : (until - now) * 0.000001f; |
| } |
| } |
| |
| final class OrientationSensorJudge extends OrientationJudge { |
| private boolean mTouching; |
| private long mTouchEndedTimestampNanos = Long.MIN_VALUE; |
| private int mProposedRotation = -1; |
| private int mDesiredRotation = -1; |
| private boolean mRotationEvaluationScheduled; |
| |
| @Override |
| public int getProposedRotationLocked() { |
| return mProposedRotation; |
| } |
| |
| @Override |
| public void onTouchStartLocked() { |
| mTouching = true; |
| } |
| |
| @Override |
| public void onTouchEndLocked(long whenElapsedNanos) { |
| mTouching = false; |
| mTouchEndedTimestampNanos = whenElapsedNanos; |
| if (mDesiredRotation != mProposedRotation) { |
| final long now = SystemClock.elapsedRealtimeNanos(); |
| scheduleRotationEvaluationIfNecessaryLocked(now); |
| } |
| } |
| |
| |
| @Override |
| public void onSensorChanged(SensorEvent event) { |
| int newRotation; |
| |
| int reportedRotation = (int) event.values[0]; |
| if (reportedRotation < 0 || reportedRotation > 3) { |
| return; |
| } |
| |
| synchronized (mLock) { |
| mDesiredRotation = reportedRotation; |
| newRotation = evaluateRotationChangeLocked(); |
| } |
| if (newRotation >=0) { |
| onProposedRotationChanged(newRotation); |
| } |
| } |
| |
| @Override |
| public void onAccuracyChanged(Sensor sensor, int accuracy) { } |
| |
| @Override |
| public void dumpLocked(PrintWriter pw, String prefix) { |
| pw.println(prefix + "OrientationSensorJudge"); |
| prefix += " "; |
| pw.println(prefix + "mDesiredRotation=" + Surface.rotationToString(mDesiredRotation)); |
| pw.println(prefix + "mProposedRotation=" |
| + Surface.rotationToString(mProposedRotation)); |
| pw.println(prefix + "mTouching=" + mTouching); |
| pw.println(prefix + "mTouchEndedTimestampNanos=" + mTouchEndedTimestampNanos); |
| } |
| |
| @Override |
| public void resetLocked(boolean clearCurrentRotation) { |
| if (clearCurrentRotation) { |
| mProposedRotation = -1; |
| mDesiredRotation = -1; |
| } |
| mTouching = false; |
| mTouchEndedTimestampNanos = Long.MIN_VALUE; |
| unscheduleRotationEvaluationLocked(); |
| } |
| |
| public int evaluateRotationChangeLocked() { |
| unscheduleRotationEvaluationLocked(); |
| if (mDesiredRotation == mProposedRotation) { |
| return -1; |
| } |
| final long now = SystemClock.elapsedRealtimeNanos(); |
| if (isDesiredRotationAcceptableLocked(now)) { |
| mProposedRotation = mDesiredRotation; |
| return mProposedRotation; |
| } else { |
| scheduleRotationEvaluationIfNecessaryLocked(now); |
| } |
| return -1; |
| } |
| |
| private boolean isDesiredRotationAcceptableLocked(long now) { |
| if (mTouching) { |
| return false; |
| } |
| if (now < mTouchEndedTimestampNanos + PROPOSAL_MIN_TIME_SINCE_TOUCH_END_NANOS) { |
| return false; |
| } |
| return true; |
| } |
| |
| private void scheduleRotationEvaluationIfNecessaryLocked(long now) { |
| if (mRotationEvaluationScheduled || mDesiredRotation == mProposedRotation) { |
| if (LOG) { |
| Slog.d(TAG, "scheduleRotationEvaluationLocked: " + |
| "ignoring, an evaluation is already scheduled or is unnecessary."); |
| } |
| return; |
| } |
| if (mTouching) { |
| if (LOG) { |
| Slog.d(TAG, "scheduleRotationEvaluationLocked: " + |
| "ignoring, user is still touching the screen."); |
| } |
| return; |
| } |
| long timeOfNextPossibleRotationNanos = |
| mTouchEndedTimestampNanos + PROPOSAL_MIN_TIME_SINCE_TOUCH_END_NANOS; |
| if (now >= timeOfNextPossibleRotationNanos) { |
| if (LOG) { |
| Slog.d(TAG, "scheduleRotationEvaluationLocked: " + |
| "ignoring, already past the next possible time of rotation."); |
| } |
| return; |
| } |
| // Use a delay instead of an absolute time since handlers are in uptime millis and we |
| // use elapsed realtime. |
| final long delayMs = |
| (long) Math.ceil((timeOfNextPossibleRotationNanos - now) * MILLIS_PER_NANO); |
| mHandler.postDelayed(mRotationEvaluator, delayMs); |
| mRotationEvaluationScheduled = true; |
| } |
| |
| private void unscheduleRotationEvaluationLocked() { |
| if (!mRotationEvaluationScheduled) { |
| return; |
| } |
| mHandler.removeCallbacks(mRotationEvaluator); |
| mRotationEvaluationScheduled = false; |
| } |
| |
| private Runnable mRotationEvaluator = new Runnable() { |
| @Override |
| public void run() { |
| int newRotation; |
| synchronized (mLock) { |
| mRotationEvaluationScheduled = false; |
| newRotation = evaluateRotationChangeLocked(); |
| } |
| if (newRotation >= 0) { |
| onProposedRotationChanged(newRotation); |
| } |
| } |
| }; |
| } |
| } |