/*
|
* 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 writeToProto(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;
|
synchronized (mLock) {
|
mDesiredRotation = (int) event.values[0];
|
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);
|
}
|
}
|
};
|
}
|
}
|