diff options
Diffstat (limited to 'widget/gonk/ProcessOrientation.cpp')
-rw-r--r-- | widget/gonk/ProcessOrientation.cpp | 519 |
1 files changed, 519 insertions, 0 deletions
diff --git a/widget/gonk/ProcessOrientation.cpp b/widget/gonk/ProcessOrientation.cpp new file mode 100644 index 000000000..bbdcface8 --- /dev/null +++ b/widget/gonk/ProcessOrientation.cpp @@ -0,0 +1,519 @@ +/* + * Copyright (c) 2013, Linux Foundation. All rights reserved + * + * 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. + */ + +#include "base/basictypes.h" +#include "mozilla/Hal.h" +#include "mozilla/Unused.h" +#include "nsIScreen.h" +#include "nsIScreenManager.h" +#include "OrientationObserver.h" +#include "ProcessOrientation.h" +#include "mozilla/HalSensor.h" +#include "math.h" +#include "limits.h" +#include "android/log.h" + +#if 0 +#define LOGD(args...) __android_log_print(ANDROID_LOG_DEBUG, "ProcessOrientation" , ## args) +#else +#define LOGD(args...) +#endif + +namespace mozilla { + +// We work with all angles in degrees in this class. +#define RADIANS_TO_DEGREES (180/M_PI) + +// Number of nanoseconds per millisecond. +#define NANOS_PER_MS 1000000 + +// Indices into SensorEvent.values for the accelerometer sensor. +#define ACCELEROMETER_DATA_X 0 +#define ACCELEROMETER_DATA_Y 1 +#define 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. +#define 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. +#define 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. +#define 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. +#define 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). +#define FLAT_ANGLE 75 +#define 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). +#define SWING_AWAY_ANGLE_DELTA 20 +#define 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. +#define 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. Android +// default is 200. +#define 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. +#define NEAR_ZERO_MAGNITUDE 1 // m/s^2 +#define ACCELERATION_TOLERANCE 4 // m/s^2 +#define STANDARD_GRAVITY 9.80665f +#define MIN_ACCELERATION_MAGNITUDE (STANDARD_GRAVITY-ACCELERATION_TOLERANCE) +#define MAX_ACCELERATION_MAGNITUDE (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. +#define MAX_TILT 75 + +// 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. +#define ADJACENT_ORIENTATION_ANGLE_GAP 45 + +const int +ProcessOrientation::tiltTolerance[][4] = { + {-25, 70}, // ROTATION_0 + {-25, 65}, // ROTATION_90 + {-25, 60}, // ROTATION_180 + {-25, 65} // ROTATION_270 +}; + +int +ProcessOrientation::GetProposedRotation() +{ + return mProposedRotation; +} + +int +ProcessOrientation::OnSensorChanged(const SensorData& event, + int deviceCurrentRotation) +{ + // 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. + const InfallibleTArray<float>& values = event.values(); + float x = values[ACCELEROMETER_DATA_X]; + float y = values[ACCELEROMETER_DATA_Y]; + float z = values[ACCELEROMETER_DATA_Z]; + + LOGD + ("ProcessOrientation: Raw acceleration vector: x = %f, y = %f, z = %f," + "magnitude = %f\n", x, y, z, 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. + const int64_t now = (int64_t) event.timestamp(); + const int64_t then = mLastFilteredTimestampNanos; + const float timeDeltaMS = (now - then) * 0.000001f; + bool skipSample = false; + if (now < then + || now > then + MAX_FILTER_DELTA_TIME_NANOS + || (x == 0 && y == 0 && z == 0)) { + LOGD + ("ProcessOrientation: Resetting orientation listener."); + Reset(); + skipSample = true; + } else { + const float alpha = timeDeltaMS / (FILTER_TIME_CONSTANT_MS + timeDeltaMS); + x = alpha * (x - mLastFilteredX) + mLastFilteredX; + y = alpha * (y - mLastFilteredY) + mLastFilteredY; + z = alpha * (z - mLastFilteredZ) + mLastFilteredZ; + LOGD + ("ProcessOrientation: Filtered acceleration vector: x=%f, y=%f, z=%f," + "magnitude=%f", z, y, z, sqrt(x * x + y * y + z * z)); + skipSample = false; + } + mLastFilteredTimestampNanos = now; + mLastFilteredX = x; + mLastFilteredY = y; + mLastFilteredZ = z; + + bool isAccelerating = false; + bool isFlat = false; + bool isSwinging = false; + if (skipSample) { + return -1; + } + + // Calculate the magnitude of the acceleration vector. + const float magnitude = sqrt(x * x + y * y + z * z); + if (magnitude < NEAR_ZERO_MAGNITUDE) { + LOGD + ("ProcessOrientation: Ignoring sensor data, magnitude too close to" + " zero."); + ClearPredictedRotation(); + } else { + // Determine whether the device appears to be undergoing external + // acceleration. + if (this->IsAccelerating(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) + const int tiltAngle = + static_cast<int>(roundf(asin(z / magnitude) * RADIANS_TO_DEGREES)); + AddTiltHistoryEntry(now, tiltAngle); + + // Determine whether the device appears to be flat or swinging. + if (this->IsFlat(now)) { + isFlat = true; + mFlatTimestampNanos = now; + } + if (this->IsSwinging(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 (abs(tiltAngle) > MAX_TILT) { + LOGD + ("ProcessOrientation: Ignoring sensor data, tilt angle too high:" + " tiltAngle=%d", tiltAngle); + ClearPredictedRotation(); + } 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 = + static_cast<int>(roundf(-atan2f(-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 (IsTiltAngleAcceptable(nearestRotation, tiltAngle) + && + IsOrientationAngleAcceptable + (nearestRotation, orientationAngle, deviceCurrentRotation)) { + UpdatePredictedRotation(now, nearestRotation); + LOGD + ("ProcessOrientation: Predicted: tiltAngle=%d, orientationAngle=%d," + " predictedRotation=%d, predictedRotationAgeMS=%f", + tiltAngle, + orientationAngle, + mPredictedRotation, + ((now - mPredictedRotationTimestampNanos) * 0.000001f)); + } else { + LOGD + ("ProcessOrientation: Ignoring sensor data, no predicted rotation:" + " tiltAngle=%d, orientationAngle=%d", + tiltAngle, + orientationAngle); + ClearPredictedRotation(); + } + } + } + + // Determine new proposed rotation. + const int oldProposedRotation = mProposedRotation; + if (mPredictedRotation < 0 || IsPredictedRotationAcceptable(now)) { + mProposedRotation = mPredictedRotation; + } + // Write final statistics about where we are in the orientation detection + // process. + LOGD + ("ProcessOrientation: Result: oldProposedRotation=%d,currentRotation=%d, " + "proposedRotation=%d, predictedRotation=%d, timeDeltaMS=%f, " + "isAccelerating=%d, isFlat=%d, isSwinging=%d, timeUntilSettledMS=%f, " + "timeUntilAccelerationDelayExpiredMS=%f, timeUntilFlatDelayExpiredMS=%f, " + "timeUntilSwingDelayExpiredMS=%f", + oldProposedRotation, + deviceCurrentRotation, mProposedRotation, + mPredictedRotation, timeDeltaMS, isAccelerating, isFlat, + isSwinging, RemainingMS(now, + mPredictedRotationTimestampNanos + + PROPOSAL_SETTLE_TIME_NANOS), + RemainingMS(now, + mAccelerationTimestampNanos + + PROPOSAL_MIN_TIME_SINCE_ACCELERATION_ENDED_NANOS), + RemainingMS(now, + mFlatTimestampNanos + + PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS), + RemainingMS(now, + mSwingTimestampNanos + + PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS)); + + // Avoid unused-but-set compile warnings for these variables, when LOGD is + // a no-op, as it is by default: + Unused << isAccelerating; + Unused << isFlat; + Unused << isSwinging; + + // Tell the listener. + if (mProposedRotation != oldProposedRotation && mProposedRotation >= 0) { + LOGD + ("ProcessOrientation: Proposed rotation changed! proposedRotation=%d, " + "oldProposedRotation=%d", + mProposedRotation, + oldProposedRotation); + return mProposedRotation; + } + // Don't rotate screen + return -1; +} + +bool +ProcessOrientation::IsTiltAngleAcceptable(int rotation, int tiltAngle) +{ + return (tiltAngle >= tiltTolerance[rotation][0] + && tiltAngle <= tiltTolerance[rotation][1]); +} + +bool +ProcessOrientation::IsOrientationAngleAcceptable(int rotation, + int orientationAngle, + int currentRotation) +{ + // 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. + if (currentRotation < 0) { + return true; + } + // 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; +} + +bool +ProcessOrientation::IsPredictedRotationAcceptable(int64_t 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; + } + // Looks good! + return true; +} + +int +ProcessOrientation::Reset() +{ + mLastFilteredTimestampNanos = std::numeric_limits<int64_t>::min(); + mProposedRotation = -1; + mFlatTimestampNanos = std::numeric_limits<int64_t>::min(); + mSwingTimestampNanos = std::numeric_limits<int64_t>::min(); + mAccelerationTimestampNanos = std::numeric_limits<int64_t>::min(); + ClearPredictedRotation(); + ClearTiltHistory(); + return -1; +} + +void +ProcessOrientation::ClearPredictedRotation() +{ + mPredictedRotation = -1; + mPredictedRotationTimestampNanos = std::numeric_limits<int64_t>::min(); +} + +void +ProcessOrientation::UpdatePredictedRotation(int64_t now, int rotation) +{ + if (mPredictedRotation != rotation) { + mPredictedRotation = rotation; + mPredictedRotationTimestampNanos = now; + } +} + +bool +ProcessOrientation::IsAccelerating(float magnitude) +{ + return magnitude < MIN_ACCELERATION_MAGNITUDE + || magnitude > MAX_ACCELERATION_MAGNITUDE; +} + +void +ProcessOrientation::ClearTiltHistory() +{ + mTiltHistory.history[0].timestampNanos = std::numeric_limits<int64_t>::min(); + mTiltHistory.index = 1; +} + +void +ProcessOrientation::AddTiltHistoryEntry(int64_t now, float tilt) +{ + mTiltHistory.history[mTiltHistory.index].tiltAngle = tilt; + mTiltHistory.history[mTiltHistory.index].timestampNanos = now; + mTiltHistory.index = (mTiltHistory.index + 1) % TILT_HISTORY_SIZE; + mTiltHistory.history[mTiltHistory.index].timestampNanos = std::numeric_limits<int64_t>::min(); +} + +bool +ProcessOrientation::IsFlat(int64_t now) +{ + for (int i = mTiltHistory.index; (i = NextTiltHistoryIndex(i)) >= 0;) { + if (mTiltHistory.history[i].tiltAngle < FLAT_ANGLE) { + break; + } + if (mTiltHistory.history[i].timestampNanos + FLAT_TIME_NANOS <= now) { + // Tilt has remained greater than FLAT_TILT_ANGLE for FLAT_TIME_NANOS. + return true; + } + } + return false; +} + +bool +ProcessOrientation::IsSwinging(int64_t now, float tilt) +{ + for (int i = mTiltHistory.index; (i = NextTiltHistoryIndex(i)) >= 0;) { + if (mTiltHistory.history[i].timestampNanos + SWING_TIME_NANOS < now) { + break; + } + if (mTiltHistory.history[i].tiltAngle + SWING_AWAY_ANGLE_DELTA <= tilt) { + // Tilted away by SWING_AWAY_ANGLE_DELTA within SWING_TIME_NANOS. + return true; + } + } + return false; +} + +int +ProcessOrientation::NextTiltHistoryIndex(int index) +{ + index = (index == 0 ? TILT_HISTORY_SIZE : index) - 1; + return mTiltHistory.history[index].timestampNanos != std::numeric_limits<int64_t>::min() ? index : -1; +} + +float +ProcessOrientation::RemainingMS(int64_t now, int64_t until) +{ + return now >= until ? 0 : (until - now) * 0.000001f; +} + +} // namespace mozilla |