michael@0: /* michael@0: * Copyright (c) 2013, Linux Foundation. All rights reserved michael@0: * michael@0: * Copyright (C) 2008 The Android Open Source Project michael@0: * michael@0: * Licensed under the Apache License, Version 2.0 (the "License"); michael@0: * you may not use this file except in compliance with the License. michael@0: * You may obtain a copy of the License at michael@0: * michael@0: * http://www.apache.org/licenses/LICENSE-2.0 michael@0: * michael@0: * Unless required by applicable law or agreed to in writing, software michael@0: * distributed under the License is distributed on an "AS IS" BASIS, michael@0: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. michael@0: * See the License for the specific language governing permissions and michael@0: * limitations under the License. michael@0: */ michael@0: michael@0: #include "base/basictypes.h" michael@0: #include "mozilla/Hal.h" michael@0: #include "nsIScreen.h" michael@0: #include "nsIScreenManager.h" michael@0: #include "OrientationObserver.h" michael@0: #include "ProcessOrientation.h" michael@0: #include "mozilla/HalSensor.h" michael@0: #include "math.h" michael@0: #include "limits.h" michael@0: #include "android/log.h" michael@0: michael@0: #if 0 michael@0: #define LOGD(args...) __android_log_print(ANDROID_LOG_DEBUG, "ProcessOrientation" , ## args) michael@0: #else michael@0: #define LOGD(args...) michael@0: #endif michael@0: michael@0: namespace mozilla { michael@0: michael@0: // We work with all angles in degrees in this class. michael@0: #define RADIANS_TO_DEGREES (180/M_PI) michael@0: michael@0: // Number of nanoseconds per millisecond. michael@0: #define NANOS_PER_MS 1000000 michael@0: michael@0: // Indices into SensorEvent.values for the accelerometer sensor. michael@0: #define ACCELEROMETER_DATA_X 0 michael@0: #define ACCELEROMETER_DATA_Y 1 michael@0: #define ACCELEROMETER_DATA_Z 2 michael@0: michael@0: // The minimum amount of time that a predicted rotation must be stable before michael@0: // it is accepted as a valid rotation proposal. This value can be quite small michael@0: // because the low-pass filter already suppresses most of the noise so we're michael@0: // really just looking for quick confirmation that the last few samples are in michael@0: // agreement as to the desired orientation. michael@0: #define PROPOSAL_SETTLE_TIME_NANOS (40*NANOS_PER_MS) michael@0: michael@0: // The minimum amount of time that must have elapsed since the device last michael@0: // exited the flat state (time since it was picked up) before the proposed michael@0: // rotation can change. michael@0: #define PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS (500*NANOS_PER_MS) michael@0: michael@0: // The minimum amount of time that must have elapsed since the device stopped michael@0: // swinging (time since device appeared to be in the process of being put down michael@0: // or put away into a pocket) before the proposed rotation can change. michael@0: #define PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS (300*NANOS_PER_MS) michael@0: michael@0: // The minimum amount of time that must have elapsed since the device stopped michael@0: // undergoing external acceleration before the proposed rotation can change. michael@0: #define PROPOSAL_MIN_TIME_SINCE_ACCELERATION_ENDED_NANOS (500*NANOS_PER_MS) michael@0: michael@0: // If the tilt angle remains greater than the specified angle for a minimum of michael@0: // the specified time, then the device is deemed to be lying flat michael@0: // (just chillin' on a table). michael@0: #define FLAT_ANGLE 75 michael@0: #define FLAT_TIME_NANOS (1000*NANOS_PER_MS) michael@0: michael@0: // If the tilt angle has increased by at least delta degrees within the michael@0: // specified amount of time, then the device is deemed to be swinging away michael@0: // from the user down towards flat (tilt = 90). michael@0: #define SWING_AWAY_ANGLE_DELTA 20 michael@0: #define SWING_TIME_NANOS (300*NANOS_PER_MS) michael@0: michael@0: // The maximum sample inter-arrival time in milliseconds. If the acceleration michael@0: // samples are further apart than this amount in time, we reset the state of michael@0: // the low-pass filter and orientation properties. This helps to handle michael@0: // boundary conditions when the device is turned on, wakes from suspend or michael@0: // there is a significant gap in samples. michael@0: #define MAX_FILTER_DELTA_TIME_NANOS (1000*NANOS_PER_MS) michael@0: michael@0: // The acceleration filter time constant. michael@0: // michael@0: // This time constant is used to tune the acceleration filter such that michael@0: // impulses and vibrational noise (think car dock) is suppressed before we try michael@0: // to calculate the tilt and orientation angles. michael@0: // michael@0: // The filter time constant is related to the filter cutoff frequency, which michael@0: // is the frequency at which signals are attenuated by 3dB (half the passband michael@0: // power). Each successive octave beyond this frequency is attenuated by an michael@0: // additional 6dB. michael@0: // michael@0: // Given a time constant t in seconds, the filter cutoff frequency Fc in Hertz michael@0: // is given by Fc = 1 / (2pi * t). michael@0: // michael@0: // The higher the time constant, the lower the cutoff frequency, so more noise michael@0: // will be suppressed. michael@0: // michael@0: // Filtering adds latency proportional the time constant (inversely michael@0: // proportional to the cutoff frequency) so we don't want to make the time michael@0: // constant too large or we can lose responsiveness. Likewise we don't want michael@0: // to make it too small or we do a poor job suppressing acceleration spikes. michael@0: // Empirically, 100ms seems to be too small and 500ms is too large. Android michael@0: // default is 200. michael@0: #define FILTER_TIME_CONSTANT_MS 200.0f michael@0: michael@0: // State for orientation detection. Thresholds for minimum and maximum michael@0: // allowable deviation from gravity. michael@0: // michael@0: // If the device is undergoing external acceleration (being bumped, in a car michael@0: // that is turning around a corner or a plane taking off) then the magnitude michael@0: // may be substantially more or less than gravity. This can skew our michael@0: // orientation detection by making us think that up is pointed in a different michael@0: // direction. michael@0: // michael@0: // Conversely, if the device is in freefall, then there will be no gravity to michael@0: // measure at all. This is problematic because we cannot detect the orientation michael@0: // without gravity to tell us which way is up. A magnitude near 0 produces michael@0: // singularities in the tilt and orientation calculations. michael@0: // michael@0: // In both cases, we postpone choosing an orientation. michael@0: // michael@0: // However, we need to tolerate some acceleration because the angular momentum michael@0: // of turning the device can skew the observed acceleration for a short period michael@0: // of time. michael@0: #define NEAR_ZERO_MAGNITUDE 1 // m/s^2 michael@0: #define ACCELERATION_TOLERANCE 4 // m/s^2 michael@0: #define STANDARD_GRAVITY 9.80665f michael@0: #define MIN_ACCELERATION_MAGNITUDE (STANDARD_GRAVITY-ACCELERATION_TOLERANCE) michael@0: #define MAX_ACCELERATION_MAGNITUDE (STANDARD_GRAVITY+ACCELERATION_TOLERANCE) michael@0: michael@0: // Maximum absolute tilt angle at which to consider orientation data. Beyond michael@0: // this (i.e. when screen is facing the sky or ground), we completely ignore michael@0: // orientation data. michael@0: #define MAX_TILT 75 michael@0: michael@0: // The gap angle in degrees between adjacent orientation angles for michael@0: // hysteresis.This creates a "dead zone" between the current orientation and a michael@0: // proposed adjacent orientation. No orientation proposal is made when the michael@0: // orientation angle is within the gap between the current orientation and the michael@0: // adjacent orientation. michael@0: #define ADJACENT_ORIENTATION_ANGLE_GAP 45 michael@0: michael@0: const int michael@0: ProcessOrientation::tiltTolerance[][4] = { michael@0: {-25, 70}, // ROTATION_0 michael@0: {-25, 65}, // ROTATION_90 michael@0: {-25, 60}, // ROTATION_180 michael@0: {-25, 65} // ROTATION_270 michael@0: }; michael@0: michael@0: int michael@0: ProcessOrientation::GetProposedRotation() michael@0: { michael@0: return mProposedRotation; michael@0: } michael@0: michael@0: int michael@0: ProcessOrientation::OnSensorChanged(const SensorData& event, michael@0: int deviceCurrentRotation) michael@0: { michael@0: // The vector given in the SensorEvent points straight up (towards the sky) michael@0: // under ideal conditions (the phone is not accelerating). I'll call this up michael@0: // vector elsewhere. michael@0: const InfallibleTArray& values = event.values(); michael@0: float x = values[ACCELEROMETER_DATA_X]; michael@0: float y = values[ACCELEROMETER_DATA_Y]; michael@0: float z = values[ACCELEROMETER_DATA_Z]; michael@0: michael@0: LOGD michael@0: ("ProcessOrientation: Raw acceleration vector: x = %f, y = %f, z = %f," michael@0: "magnitude = %f\n", x, y, z, sqrt(x * x + y * y + z * z)); michael@0: // Apply a low-pass filter to the acceleration up vector in cartesian space. michael@0: // Reset the orientation listener state if the samples are too far apart in michael@0: // time or when we see values of (0, 0, 0) which indicates that we polled the michael@0: // accelerometer too soon after turning it on and we don't have any data yet. michael@0: const long now = event.timestamp(); michael@0: const long then = mLastFilteredTimestampNanos; michael@0: const float timeDeltaMS = (now - then) * 0.000001f; michael@0: bool skipSample = false; michael@0: if (now < then michael@0: || now > then + MAX_FILTER_DELTA_TIME_NANOS michael@0: || (x == 0 && y == 0 && z == 0)) { michael@0: LOGD michael@0: ("ProcessOrientation: Resetting orientation listener."); michael@0: Reset(); michael@0: skipSample = true; michael@0: } else { michael@0: const float alpha = timeDeltaMS / (FILTER_TIME_CONSTANT_MS + timeDeltaMS); michael@0: x = alpha * (x - mLastFilteredX) + mLastFilteredX; michael@0: y = alpha * (y - mLastFilteredY) + mLastFilteredY; michael@0: z = alpha * (z - mLastFilteredZ) + mLastFilteredZ; michael@0: LOGD michael@0: ("ProcessOrientation: Filtered acceleration vector: x=%f, y=%f, z=%f," michael@0: "magnitude=%f", z, y, z, sqrt(x * x + y * y + z * z)); michael@0: skipSample = false; michael@0: } michael@0: mLastFilteredTimestampNanos = now; michael@0: mLastFilteredX = x; michael@0: mLastFilteredY = y; michael@0: mLastFilteredZ = z; michael@0: michael@0: bool isAccelerating = false; michael@0: bool isFlat = false; michael@0: bool isSwinging = false; michael@0: if (skipSample) { michael@0: return -1; michael@0: } michael@0: michael@0: // Calculate the magnitude of the acceleration vector. michael@0: const float magnitude = sqrt(x * x + y * y + z * z); michael@0: if (magnitude < NEAR_ZERO_MAGNITUDE) { michael@0: LOGD michael@0: ("ProcessOrientation: Ignoring sensor data, magnitude too close to" michael@0: " zero."); michael@0: ClearPredictedRotation(); michael@0: } else { michael@0: // Determine whether the device appears to be undergoing external michael@0: // acceleration. michael@0: if (this->IsAccelerating(magnitude)) { michael@0: isAccelerating = true; michael@0: mAccelerationTimestampNanos = now; michael@0: } michael@0: // Calculate the tilt angle. This is the angle between the up vector and michael@0: // the x-y plane (the plane of the screen) in a range of [-90, 90] michael@0: // degrees. michael@0: // -90 degrees: screen horizontal and facing the ground (overhead) michael@0: // 0 degrees: screen vertical michael@0: // 90 degrees: screen horizontal and facing the sky (on table) michael@0: const int tiltAngle = michael@0: static_cast(roundf(asin(z / magnitude) * RADIANS_TO_DEGREES)); michael@0: AddTiltHistoryEntry(now, tiltAngle); michael@0: michael@0: // Determine whether the device appears to be flat or swinging. michael@0: if (this->IsFlat(now)) { michael@0: isFlat = true; michael@0: mFlatTimestampNanos = now; michael@0: } michael@0: if (this->IsSwinging(now, tiltAngle)) { michael@0: isSwinging = true; michael@0: mSwingTimestampNanos = now; michael@0: } michael@0: // If the tilt angle is too close to horizontal then we cannot determine michael@0: // the orientation angle of the screen. michael@0: if (abs(tiltAngle) > MAX_TILT) { michael@0: LOGD michael@0: ("ProcessOrientation: Ignoring sensor data, tilt angle too high:" michael@0: " tiltAngle=%d", tiltAngle); michael@0: ClearPredictedRotation(); michael@0: } else { michael@0: // Calculate the orientation angle. michael@0: // This is the angle between the x-y projection of the up vector onto michael@0: // the +y-axis, increasing clockwise in a range of [0, 360] degrees. michael@0: int orientationAngle = michael@0: static_cast(roundf(-atan2f(-x, y) * RADIANS_TO_DEGREES)); michael@0: if (orientationAngle < 0) { michael@0: // atan2 returns [-180, 180]; normalize to [0, 360] michael@0: orientationAngle += 360; michael@0: } michael@0: // Find the nearest rotation. michael@0: int nearestRotation = (orientationAngle + 45) / 90; michael@0: if (nearestRotation == 4) { michael@0: nearestRotation = 0; michael@0: } michael@0: // Determine the predicted orientation. michael@0: if (IsTiltAngleAcceptable(nearestRotation, tiltAngle) michael@0: && michael@0: IsOrientationAngleAcceptable michael@0: (nearestRotation, orientationAngle, deviceCurrentRotation)) { michael@0: UpdatePredictedRotation(now, nearestRotation); michael@0: LOGD michael@0: ("ProcessOrientation: Predicted: tiltAngle=%d, orientationAngle=%d," michael@0: " predictedRotation=%d, predictedRotationAgeMS=%f", michael@0: tiltAngle, michael@0: orientationAngle, michael@0: mPredictedRotation, michael@0: ((now - mPredictedRotationTimestampNanos) * 0.000001f)); michael@0: } else { michael@0: LOGD michael@0: ("ProcessOrientation: Ignoring sensor data, no predicted rotation:" michael@0: " tiltAngle=%d, orientationAngle=%d", michael@0: tiltAngle, michael@0: orientationAngle); michael@0: ClearPredictedRotation(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Determine new proposed rotation. michael@0: const int oldProposedRotation = mProposedRotation; michael@0: if (mPredictedRotation < 0 || IsPredictedRotationAcceptable(now)) { michael@0: mProposedRotation = mPredictedRotation; michael@0: } michael@0: // Write final statistics about where we are in the orientation detection michael@0: // process. michael@0: LOGD michael@0: ("ProcessOrientation: Result: oldProposedRotation=%d,currentRotation=%d, " michael@0: "proposedRotation=%d, predictedRotation=%d, timeDeltaMS=%f, " michael@0: "isAccelerating=%d, isFlat=%d, isSwinging=%d, timeUntilSettledMS=%f, " michael@0: "timeUntilAccelerationDelayExpiredMS=%f, timeUntilFlatDelayExpiredMS=%f, " michael@0: "timeUntilSwingDelayExpiredMS=%f", michael@0: oldProposedRotation, michael@0: deviceCurrentRotation, mProposedRotation, michael@0: mPredictedRotation, timeDeltaMS, isAccelerating, isFlat, michael@0: isSwinging, RemainingMS(now, michael@0: mPredictedRotationTimestampNanos + michael@0: PROPOSAL_SETTLE_TIME_NANOS), michael@0: RemainingMS(now, michael@0: mAccelerationTimestampNanos + michael@0: PROPOSAL_MIN_TIME_SINCE_ACCELERATION_ENDED_NANOS), michael@0: RemainingMS(now, michael@0: mFlatTimestampNanos + michael@0: PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS), michael@0: RemainingMS(now, michael@0: mSwingTimestampNanos + michael@0: PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS)); michael@0: // Tell the listener. michael@0: if (mProposedRotation != oldProposedRotation && mProposedRotation >= 0) { michael@0: LOGD michael@0: ("ProcessOrientation: Proposed rotation changed! proposedRotation=%d, " michael@0: "oldProposedRotation=%d", michael@0: mProposedRotation, michael@0: oldProposedRotation); michael@0: return mProposedRotation; michael@0: } michael@0: // Don't rotate screen michael@0: return -1; michael@0: } michael@0: michael@0: bool michael@0: ProcessOrientation::IsTiltAngleAcceptable(int rotation, int tiltAngle) michael@0: { michael@0: return (tiltAngle >= tiltTolerance[rotation][0] michael@0: && tiltAngle <= tiltTolerance[rotation][1]); michael@0: } michael@0: michael@0: bool michael@0: ProcessOrientation::IsOrientationAngleAcceptable(int rotation, michael@0: int orientationAngle, michael@0: int currentRotation) michael@0: { michael@0: // If there is no current rotation, then there is no gap. michael@0: // The gap is used only to introduce hysteresis among advertised orientation michael@0: // changes to avoid flapping. michael@0: if (currentRotation < 0) { michael@0: return true; michael@0: } michael@0: // If the specified rotation is the same or is counter-clockwise adjacent michael@0: // to the current rotation, then we set a lower bound on the orientation michael@0: // angle. For example, if currentRotation is ROTATION_0 and proposed is michael@0: // ROTATION_90, then we want to check orientationAngle > 45 + GAP / 2. michael@0: if (rotation == currentRotation || rotation == (currentRotation + 1) % 4) { michael@0: int lowerBound = rotation * 90 - 45 + ADJACENT_ORIENTATION_ANGLE_GAP / 2; michael@0: if (rotation == 0) { michael@0: if (orientationAngle >= 315 && orientationAngle < lowerBound + 360) { michael@0: return false; michael@0: } michael@0: } else { michael@0: if (orientationAngle < lowerBound) { michael@0: return false; michael@0: } michael@0: } michael@0: } michael@0: // If the specified rotation is the same or is clockwise adjacent, then we michael@0: // set an upper bound on the orientation angle. For example, if michael@0: // currentRotation is ROTATION_0 and rotation is ROTATION_270, then we want michael@0: // to check orientationAngle < 315 - GAP / 2. michael@0: if (rotation == currentRotation || rotation == (currentRotation + 3) % 4) { michael@0: int upperBound = rotation * 90 + 45 - ADJACENT_ORIENTATION_ANGLE_GAP / 2; michael@0: if (rotation == 0) { michael@0: if (orientationAngle <= 45 && orientationAngle > upperBound) { michael@0: return false; michael@0: } michael@0: } else { michael@0: if (orientationAngle > upperBound) { michael@0: return false; michael@0: } michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: bool michael@0: ProcessOrientation::IsPredictedRotationAcceptable(long now) michael@0: { michael@0: // The predicted rotation must have settled long enough. michael@0: if (now < mPredictedRotationTimestampNanos + PROPOSAL_SETTLE_TIME_NANOS) { michael@0: return false; michael@0: } michael@0: // The last flat state (time since picked up) must have been sufficiently long michael@0: // ago. michael@0: if (now < mFlatTimestampNanos + PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS) { michael@0: return false; michael@0: } michael@0: // The last swing state (time since last movement to put down) must have been michael@0: // sufficiently long ago. michael@0: if (now < mSwingTimestampNanos + PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS) { michael@0: return false; michael@0: } michael@0: // The last acceleration state must have been sufficiently long ago. michael@0: if (now < mAccelerationTimestampNanos michael@0: + PROPOSAL_MIN_TIME_SINCE_ACCELERATION_ENDED_NANOS) { michael@0: return false; michael@0: } michael@0: // Looks good! michael@0: return true; michael@0: } michael@0: michael@0: int michael@0: ProcessOrientation::Reset() michael@0: { michael@0: mLastFilteredTimestampNanos = LONG_MIN; michael@0: mProposedRotation = -1; michael@0: mFlatTimestampNanos = LONG_MIN; michael@0: mSwingTimestampNanos = LONG_MIN; michael@0: mAccelerationTimestampNanos = LONG_MIN; michael@0: ClearPredictedRotation(); michael@0: ClearTiltHistory(); michael@0: return -1; michael@0: } michael@0: michael@0: void michael@0: ProcessOrientation::ClearPredictedRotation() michael@0: { michael@0: mPredictedRotation = -1; michael@0: mPredictedRotationTimestampNanos = LONG_MIN; michael@0: } michael@0: michael@0: void michael@0: ProcessOrientation::UpdatePredictedRotation(long now, int rotation) michael@0: { michael@0: if (mPredictedRotation != rotation) { michael@0: mPredictedRotation = rotation; michael@0: mPredictedRotationTimestampNanos = now; michael@0: } michael@0: } michael@0: michael@0: bool michael@0: ProcessOrientation::IsAccelerating(float magnitude) michael@0: { michael@0: return magnitude < MIN_ACCELERATION_MAGNITUDE michael@0: || magnitude > MAX_ACCELERATION_MAGNITUDE; michael@0: } michael@0: michael@0: void michael@0: ProcessOrientation::ClearTiltHistory() michael@0: { michael@0: mTiltHistory.history[0].timestampNanos = LONG_MIN; michael@0: mTiltHistory.index = 1; michael@0: } michael@0: michael@0: void michael@0: ProcessOrientation::AddTiltHistoryEntry(long now, float tilt) michael@0: { michael@0: mTiltHistory.history[mTiltHistory.index].tiltAngle = tilt; michael@0: mTiltHistory.history[mTiltHistory.index].timestampNanos = now; michael@0: mTiltHistory.index = (mTiltHistory.index + 1) % TILT_HISTORY_SIZE; michael@0: mTiltHistory.history[mTiltHistory.index].timestampNanos = LONG_MIN; michael@0: } michael@0: michael@0: bool michael@0: ProcessOrientation::IsFlat(long now) michael@0: { michael@0: for (int i = mTiltHistory.index; (i = NextTiltHistoryIndex(i)) >= 0;) { michael@0: if (mTiltHistory.history[i].tiltAngle < FLAT_ANGLE) { michael@0: break; michael@0: } michael@0: if (mTiltHistory.history[i].timestampNanos + FLAT_TIME_NANOS <= now) { michael@0: // Tilt has remained greater than FLAT_TILT_ANGLE for FLAT_TIME_NANOS. michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: bool michael@0: ProcessOrientation::IsSwinging(long now, float tilt) michael@0: { michael@0: for (int i = mTiltHistory.index; (i = NextTiltHistoryIndex(i)) >= 0;) { michael@0: if (mTiltHistory.history[i].timestampNanos + SWING_TIME_NANOS < now) { michael@0: break; michael@0: } michael@0: if (mTiltHistory.history[i].tiltAngle + SWING_AWAY_ANGLE_DELTA <= tilt) { michael@0: // Tilted away by SWING_AWAY_ANGLE_DELTA within SWING_TIME_NANOS. michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: int michael@0: ProcessOrientation::NextTiltHistoryIndex(int index) michael@0: { michael@0: index = (index == 0 ? TILT_HISTORY_SIZE : index) - 1; michael@0: return mTiltHistory.history[index].timestampNanos != LONG_MIN ? index : -1; michael@0: } michael@0: michael@0: float michael@0: ProcessOrientation::RemainingMS(long now, long until) michael@0: { michael@0: return now >= until ? 0 : (until - now) * 0.000001f; michael@0: } michael@0: michael@0: } // namespace mozilla