blob: c3c74a7c59c547bd7d5e9745f462c389c62a2950 [file] [log] [blame]
/*
* 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;
}
}
}