/* * 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