maximize_mode_controller.cc revision a02191e04bc25c4935f804f2c080ae28663d096d
1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "ash/wm/maximize_mode/maximize_mode_controller.h"
6
7#include "ash/accelerometer/accelerometer_controller.h"
8#include "ash/display/display_manager.h"
9#include "ash/shell.h"
10#include "ash/wm/maximize_mode/maximize_mode_event_blocker.h"
11#include "ui/gfx/vector3d_f.h"
12
13namespace ash {
14
15namespace {
16
17// The hinge angle at which to enter maximize mode.
18const float kEnterMaximizeModeAngle = 200.0f;
19
20// The angle at which to exit maximize mode, this is specifically less than the
21// angle to enter maximize mode to prevent rapid toggling when near the angle.
22const float kExitMaximizeModeAngle = 160.0f;
23
24// When the lid is fully open 360 degrees, the accelerometer readings can
25// occasionally appear as though the lid is almost closed. If the lid appears
26// near closed but the device is on we assume it is an erroneous reading from
27// it being open 360 degrees.
28const float kFullyOpenAngleErrorTolerance = 20.0f;
29
30// When the device approaches vertical orientation (i.e. portrait orientation)
31// the accelerometers for the base and lid approach the same values (i.e.
32// gravity pointing in the direction of the hinge). When this happens we cannot
33// compute the hinge angle reliably and must turn ignore accelerometer readings.
34// This is the angle from vertical under which we will not compute a hinge
35// angle.
36const float kHingeAxisAlignedThreshold = 15.0f;
37
38// The maximum deviation from the acceleration expected due to gravity under
39// which to detect hinge angle and screen rotation.
40const float kDeviationFromGravityThreshold = 0.1f;
41
42// The maximum deviation between the magnitude of the two accelerometers under
43// which to detect hinge angle and screen rotation. These accelerometers are
44// attached to the same physical device and so should be under the same
45// acceleration.
46const float kNoisyMagnitudeDeviation = 0.1f;
47
48// The angle which the screen has to be rotated past before the display will
49// rotate to match it (i.e. 45.0f is no stickiness).
50const float kDisplayRotationStickyAngleDegrees = 60.0f;
51
52// The minimum acceleration in a direction required to trigger screen rotation.
53// This prevents rapid toggling of rotation when the device is near flat and
54// there is very little screen aligned force on it.
55const float kMinimumAccelerationScreenRotation = 0.3f;
56
57const float kRadiansToDegrees = 180.0f / 3.14159265f;
58
59// Returns the angle between |base| and |other| in degrees.
60float AngleBetweenVectorsInDegrees(const gfx::Vector3dF& base,
61                                 const gfx::Vector3dF& other) {
62  return acos(gfx::DotProduct(base, other) /
63              base.Length() / other.Length()) * kRadiansToDegrees;
64}
65
66// Returns the clockwise angle between |base| and |other| where |normal| is the
67// normal of the virtual surface to measure clockwise according to.
68float ClockwiseAngleBetweenVectorsInDegrees(const gfx::Vector3dF& base,
69                                            const gfx::Vector3dF& other,
70                                            const gfx::Vector3dF& normal) {
71  float angle = AngleBetweenVectorsInDegrees(base, other);
72  gfx::Vector3dF cross(base);
73  cross.Cross(other);
74  // If the dot product of this cross product is normal, it means that the
75  // shortest angle between |base| and |other| was counterclockwise with respect
76  // to the surface represented by |normal| and this angle must be reversed.
77  if (gfx::DotProduct(cross, normal) > 0.0f)
78    angle = 360.0f - angle;
79  return angle;
80}
81
82}  // namespace
83
84MaximizeModeController::MaximizeModeController()
85    : rotation_locked_(false) {
86  Shell::GetInstance()->accelerometer_controller()->AddObserver(this);
87}
88
89MaximizeModeController::~MaximizeModeController() {
90  Shell::GetInstance()->accelerometer_controller()->RemoveObserver(this);
91}
92
93void MaximizeModeController::OnAccelerometerUpdated(
94    const gfx::Vector3dF& base,
95    const gfx::Vector3dF& lid) {
96  // Ignore the reading if it appears unstable. The reading is considered
97  // unstable if it deviates too much from gravity and/or the magnitude of the
98  // reading from the lid differs too much from the reading from the base.
99  float base_magnitude = base.Length();
100  float lid_magnitude = lid.Length();
101  if (std::abs(base_magnitude - lid_magnitude) > kNoisyMagnitudeDeviation ||
102      std::abs(base_magnitude - 1.0f) > kDeviationFromGravityThreshold ||
103      std::abs(lid_magnitude - 1.0f) > kDeviationFromGravityThreshold) {
104      return;
105  }
106
107  // Responding to the hinge rotation can change the maximize mode state which
108  // affects screen rotation, so we handle hinge rotation first.
109  HandleHingeRotation(base, lid);
110  HandleScreenRotation(lid);
111}
112
113void MaximizeModeController::HandleHingeRotation(const gfx::Vector3dF& base,
114                                                 const gfx::Vector3dF& lid) {
115  static const gfx::Vector3dF hinge_vector(0.0f, 1.0f, 0.0f);
116  bool maximize_mode_engaged =
117      Shell::GetInstance()->IsMaximizeModeWindowManagerEnabled();
118
119  // As the hinge approaches a vertical angle, the base and lid accelerometers
120  // approach the same values making any angle calculations highly inaccurate.
121  // Bail out early when it is too close.
122  float hinge_angle = AngleBetweenVectorsInDegrees(base, hinge_vector);
123  if (hinge_angle < kHingeAxisAlignedThreshold ||
124      hinge_angle > 180.0f - kHingeAxisAlignedThreshold) {
125    return;
126  }
127
128  // Compute the angle between the base and the lid.
129  float angle = ClockwiseAngleBetweenVectorsInDegrees(base, lid, hinge_vector);
130
131  // Toggle maximize mode on or off when corresponding thresholds are passed.
132  // TODO(flackr): Make MaximizeModeController own the MaximizeModeWindowManager
133  // such that observations of state changes occur after the change and shell
134  // has fewer states to track.
135  if (maximize_mode_engaged &&
136      angle > kFullyOpenAngleErrorTolerance &&
137      angle < kExitMaximizeModeAngle) {
138    Shell::GetInstance()->EnableMaximizeModeWindowManager(false);
139    event_blocker_.reset();
140  } else if (!maximize_mode_engaged &&
141      angle > kEnterMaximizeModeAngle) {
142    Shell::GetInstance()->EnableMaximizeModeWindowManager(true);
143    event_blocker_.reset(new MaximizeModeEventBlocker);
144  }
145}
146
147void MaximizeModeController::HandleScreenRotation(const gfx::Vector3dF& lid) {
148  bool maximize_mode_engaged =
149      Shell::GetInstance()->IsMaximizeModeWindowManagerEnabled();
150
151  DisplayManager* display_manager =
152      Shell::GetInstance()->display_manager();
153  gfx::Display::Rotation current_rotation = display_manager->GetDisplayInfo(
154      gfx::Display::InternalDisplayId()).rotation();
155
156  // If maximize mode is not engaged, ensure the screen is not rotated and
157  // do not rotate to match the current device orientation.
158  if (!maximize_mode_engaged) {
159    if (current_rotation != gfx::Display::ROTATE_0) {
160      // TODO(flackr): Currently this will prevent setting a manual rotation on
161      // the screen of a device with an accelerometer, this should only set it
162      // back to ROTATE_0 if it was last set by the accelerometer.
163      // Also, SetDisplayRotation will save the setting to the local store,
164      // this should be stored in a way that we can distinguish what the
165      // rotation was set by.
166      display_manager->SetDisplayRotation(gfx::Display::InternalDisplayId(),
167                                          gfx::Display::ROTATE_0);
168    }
169    rotation_locked_ = false;
170    return;
171  }
172
173  if (rotation_locked_)
174    return;
175
176  // After determining maximize mode state, determine if the screen should
177  // be rotated.
178  gfx::Vector3dF lid_flattened(lid.x(), lid.y(), 0.0f);
179  float lid_flattened_length = lid_flattened.Length();
180  // When the lid is close to being flat, don't change rotation as it is too
181  // sensitive to slight movements.
182  if (lid_flattened_length < kMinimumAccelerationScreenRotation)
183    return;
184
185  // The reference vector is the angle of gravity when the device is rotated
186  // clockwise by 45 degrees. Computing the angle between this vector and
187  // gravity we can easily determine the expected display rotation.
188  static gfx::Vector3dF rotation_reference(-1.0f, 1.0f, 0.0f);
189
190  // Set the down vector to match the expected direction of gravity given the
191  // last configured rotation. This is used to enforce a stickiness that the
192  // user must overcome to rotate the display and prevents frequent rotations
193  // when holding the device near 45 degrees.
194  gfx::Vector3dF down(0.0f, 0.0f, 0.0f);
195  if (current_rotation == gfx::Display::ROTATE_0)
196    down.set_x(-1.0f);
197  else if (current_rotation == gfx::Display::ROTATE_90)
198    down.set_y(1.0f);
199  else if (current_rotation == gfx::Display::ROTATE_180)
200    down.set_x(1.0f);
201  else
202    down.set_y(-1.0f);
203
204  // Don't rotate if the screen has not passed the threshold.
205  if (AngleBetweenVectorsInDegrees(down, lid_flattened) <
206      kDisplayRotationStickyAngleDegrees) {
207    return;
208  }
209
210  float angle = ClockwiseAngleBetweenVectorsInDegrees(rotation_reference,
211      lid_flattened, gfx::Vector3dF(0.0f, 0.0f, -1.0f));
212
213  gfx::Display::Rotation new_rotation = gfx::Display::ROTATE_90;
214  if (angle < 90.0f)
215    new_rotation = gfx::Display::ROTATE_0;
216  else if (angle < 180.0f)
217    new_rotation = gfx::Display::ROTATE_270;
218  else if (angle < 270.0f)
219    new_rotation = gfx::Display::ROTATE_180;
220
221  // When exiting maximize mode return rotation to 0. When entering, rotate to
222  // match screen orientation.
223  if (new_rotation == gfx::Display::ROTATE_0 ||
224      maximize_mode_engaged) {
225    display_manager->SetDisplayRotation(gfx::Display::InternalDisplayId(),
226                                        new_rotation);
227  }
228}
229
230}  // namespace ash
231