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/accelerators/accelerator_controller.h"
8#include "ash/accelerators/accelerator_table.h"
9#include "ash/accelerometer/accelerometer_controller.h"
10#include "ash/ash_switches.h"
11#include "ash/display/display_manager.h"
12#include "ash/shell.h"
13#include "ash/wm/maximize_mode/maximize_mode_window_manager.h"
14#include "ash/wm/maximize_mode/scoped_disable_internal_mouse_and_keyboard.h"
15#include "base/auto_reset.h"
16#include "base/command_line.h"
17#include "base/metrics/histogram.h"
18#include "base/time/default_tick_clock.h"
19#include "base/time/tick_clock.h"
20#include "ui/base/accelerators/accelerator.h"
21#include "ui/events/event.h"
22#include "ui/events/keycodes/keyboard_codes.h"
23#include "ui/gfx/vector3d_f.h"
24
25#if defined(USE_X11)
26#include "ash/wm/maximize_mode/scoped_disable_internal_mouse_and_keyboard_x11.h"
27#endif
28
29#if defined(OS_CHROMEOS)
30#include "chromeos/dbus/dbus_thread_manager.h"
31#endif  // OS_CHROMEOS
32
33namespace ash {
34
35namespace {
36
37// The hinge angle at which to enter maximize mode.
38const float kEnterMaximizeModeAngle = 200.0f;
39
40// The angle at which to exit maximize mode, this is specifically less than the
41// angle to enter maximize mode to prevent rapid toggling when near the angle.
42const float kExitMaximizeModeAngle = 160.0f;
43
44// Defines a range for which accelerometer readings are considered accurate.
45// When the lid is near open (or near closed) the accelerometer readings may be
46// inaccurate and a lid that is fully open may appear to be near closed (and
47// vice versa).
48const float kMinStableAngle = 20.0f;
49const float kMaxStableAngle = 340.0f;
50
51// The time duration to consider the lid to be recently opened.
52// This is used to prevent entering maximize mode if an erroneous accelerometer
53// reading makes the lid appear to be fully open when the user is opening the
54// lid from a closed position.
55const base::TimeDelta kLidRecentlyOpenedDuration =
56    base::TimeDelta::FromSeconds(2);
57
58// The mean acceleration due to gravity on Earth in m/s^2.
59const float kMeanGravity = 9.80665f;
60
61// When the device approaches vertical orientation (i.e. portrait orientation)
62// the accelerometers for the base and lid approach the same values (i.e.
63// gravity pointing in the direction of the hinge). When this happens we cannot
64// compute the hinge angle reliably and must turn ignore accelerometer readings.
65// This is the minimum acceleration perpendicular to the hinge under which to
66// detect hinge angle in m/s^2.
67const float kHingeAngleDetectionThreshold = 2.5f;
68
69// The maximum deviation from the acceleration expected due to gravity under
70// which to detect hinge angle and screen rotation in m/s^2
71const float kDeviationFromGravityThreshold = 1.0f;
72
73// The maximum deviation between the magnitude of the two accelerometers under
74// which to detect hinge angle and screen rotation in m/s^2. These
75// accelerometers are attached to the same physical device and so should be
76// under the same acceleration.
77const float kNoisyMagnitudeDeviation = 1.0f;
78
79// The angle which the screen has to be rotated past before the display will
80// rotate to match it (i.e. 45.0f is no stickiness).
81const float kDisplayRotationStickyAngleDegrees = 60.0f;
82
83// The minimum acceleration in m/s^2 in a direction required to trigger screen
84// rotation. This prevents rapid toggling of rotation when the device is near
85// flat and there is very little screen aligned force on it. The value is
86// effectively the sine of the rise angle required times the acceleration due
87// to gravity, with the current value requiring at least a 25 degree rise.
88const float kMinimumAccelerationScreenRotation = 4.2f;
89
90const float kRadiansToDegrees = 180.0f / 3.14159265f;
91
92// Returns the angle between |base| and |other| in degrees.
93float AngleBetweenVectorsInDegrees(const gfx::Vector3dF& base,
94                                 const gfx::Vector3dF& other) {
95  return acos(gfx::DotProduct(base, other) /
96              base.Length() / other.Length()) * kRadiansToDegrees;
97}
98
99// Returns the clockwise angle between |base| and |other| where |normal| is the
100// normal of the virtual surface to measure clockwise according to.
101float ClockwiseAngleBetweenVectorsInDegrees(const gfx::Vector3dF& base,
102                                            const gfx::Vector3dF& other,
103                                            const gfx::Vector3dF& normal) {
104  float angle = AngleBetweenVectorsInDegrees(base, other);
105  gfx::Vector3dF cross(base);
106  cross.Cross(other);
107  // If the dot product of this cross product is normal, it means that the
108  // shortest angle between |base| and |other| was counterclockwise with respect
109  // to the surface represented by |normal| and this angle must be reversed.
110  if (gfx::DotProduct(cross, normal) > 0.0f)
111    angle = 360.0f - angle;
112  return angle;
113}
114
115}  // namespace
116
117MaximizeModeController::MaximizeModeController()
118    : rotation_locked_(false),
119      have_seen_accelerometer_data_(false),
120      ignore_display_configuration_updates_(false),
121      shutting_down_(false),
122      user_rotation_(gfx::Display::ROTATE_0),
123      last_touchview_transition_time_(base::Time::Now()),
124      tick_clock_(new base::DefaultTickClock()),
125      lid_is_closed_(false) {
126  Shell::GetInstance()->accelerometer_controller()->AddObserver(this);
127  Shell::GetInstance()->AddShellObserver(this);
128#if defined(OS_CHROMEOS)
129  chromeos::DBusThreadManager::Get()->
130      GetPowerManagerClient()->AddObserver(this);
131#endif  // OS_CHROMEOS
132}
133
134MaximizeModeController::~MaximizeModeController() {
135  Shell::GetInstance()->RemoveShellObserver(this);
136  Shell::GetInstance()->accelerometer_controller()->RemoveObserver(this);
137#if defined(OS_CHROMEOS)
138  chromeos::DBusThreadManager::Get()->
139      GetPowerManagerClient()->RemoveObserver(this);
140#endif  // OS_CHROMEOS
141}
142
143void MaximizeModeController::SetRotationLocked(bool rotation_locked) {
144  if (rotation_locked_ == rotation_locked)
145    return;
146  base::AutoReset<bool> auto_ignore_display_configuration_updates(
147      &ignore_display_configuration_updates_, true);
148  rotation_locked_ = rotation_locked;
149  Shell::GetInstance()->display_manager()->
150      RegisterDisplayRotationProperties(rotation_locked_, current_rotation_);
151  FOR_EACH_OBSERVER(Observer, observers_,
152                    OnRotationLockChanged(rotation_locked_));
153}
154
155void MaximizeModeController::AddObserver(Observer* observer) {
156  observers_.AddObserver(observer);
157}
158
159void MaximizeModeController::RemoveObserver(Observer* observer) {
160  observers_.RemoveObserver(observer);
161}
162
163bool MaximizeModeController::CanEnterMaximizeMode() {
164  // If we have ever seen accelerometer data, then HandleHingeRotation may
165  // trigger maximize mode at some point in the future.
166  // The --enable-touch-view-testing switch can also mean that we may enter
167  // maximize mode.
168  return have_seen_accelerometer_data_ ||
169         CommandLine::ForCurrentProcess()->HasSwitch(
170             switches::kAshEnableTouchViewTesting);
171}
172
173void MaximizeModeController::EnableMaximizeModeWindowManager(bool enable) {
174  if (enable && !maximize_mode_window_manager_.get()) {
175    maximize_mode_window_manager_.reset(new MaximizeModeWindowManager());
176    // TODO(jonross): Move the maximize mode notifications from ShellObserver
177    // to MaximizeModeController::Observer
178    Shell::GetInstance()->OnMaximizeModeStarted();
179  } else if (!enable && maximize_mode_window_manager_.get()) {
180    maximize_mode_window_manager_.reset();
181    Shell::GetInstance()->OnMaximizeModeEnded();
182  }
183}
184
185bool MaximizeModeController::IsMaximizeModeWindowManagerEnabled() const {
186  return maximize_mode_window_manager_.get() != NULL;
187}
188
189void MaximizeModeController::AddWindow(aura::Window* window) {
190  if (IsMaximizeModeWindowManagerEnabled())
191    maximize_mode_window_manager_->AddWindow(window);
192}
193
194void MaximizeModeController::Shutdown() {
195  shutting_down_ = true;
196  LeaveMaximizeMode();
197}
198
199void MaximizeModeController::OnAccelerometerUpdated(
200    const ui::AccelerometerUpdate& update) {
201  bool first_accelerometer_update = !have_seen_accelerometer_data_;
202  have_seen_accelerometer_data_ = true;
203
204  // Ignore the reading if it appears unstable. The reading is considered
205  // unstable if it deviates too much from gravity and/or the magnitude of the
206  // reading from the lid differs too much from the reading from the base.
207  float base_magnitude =
208      update.has(ui::ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD) ?
209      update.get(ui::ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD).Length() :
210      0.0f;
211  float lid_magnitude = update.has(ui::ACCELEROMETER_SOURCE_SCREEN) ?
212      update.get(ui::ACCELEROMETER_SOURCE_SCREEN).Length() : 0.0f;
213  bool lid_stable = update.has(ui::ACCELEROMETER_SOURCE_SCREEN) &&
214      std::abs(lid_magnitude - kMeanGravity) <= kDeviationFromGravityThreshold;
215  bool base_angle_stable = lid_stable &&
216      update.has(ui::ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD) &&
217      std::abs(base_magnitude - lid_magnitude) <= kNoisyMagnitudeDeviation &&
218      std::abs(base_magnitude - kMeanGravity) <= kDeviationFromGravityThreshold;
219
220  if (base_angle_stable) {
221    // Responding to the hinge rotation can change the maximize mode state which
222    // affects screen rotation, so we handle hinge rotation first.
223    HandleHingeRotation(
224        update.get(ui::ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD),
225        update.get(ui::ACCELEROMETER_SOURCE_SCREEN));
226  }
227  if (lid_stable)
228    HandleScreenRotation(update.get(ui::ACCELEROMETER_SOURCE_SCREEN));
229
230  if (first_accelerometer_update) {
231    // On the first accelerometer update we will know if we have entered
232    // maximize mode or not. Update the preferences to reflect the current
233    // state.
234    Shell::GetInstance()->display_manager()->
235        RegisterDisplayRotationProperties(rotation_locked_, current_rotation_);
236  }
237}
238
239void MaximizeModeController::OnDisplayConfigurationChanged() {
240  if (ignore_display_configuration_updates_)
241    return;
242  DisplayManager* display_manager = Shell::GetInstance()->display_manager();
243  gfx::Display::Rotation user_rotation = display_manager->
244      GetDisplayInfo(gfx::Display::InternalDisplayId()).rotation();
245  if (user_rotation != current_rotation_) {
246    // A user may change other display configuration settings. When the user
247    // does change the rotation setting, then lock rotation to prevent the
248    // accelerometer from erasing their change.
249    SetRotationLocked(true);
250    user_rotation_ = user_rotation;
251    current_rotation_ = user_rotation;
252  }
253}
254
255#if defined(OS_CHROMEOS)
256void MaximizeModeController::LidEventReceived(bool open,
257                                              const base::TimeTicks& time) {
258  if (open)
259    last_lid_open_time_ = time;
260  lid_is_closed_ = !open;
261  LeaveMaximizeMode();
262}
263
264void MaximizeModeController::SuspendImminent() {
265  RecordTouchViewStateTransition();
266}
267
268void MaximizeModeController::SuspendDone(
269    const base::TimeDelta& sleep_duration) {
270  last_touchview_transition_time_ = base::Time::Now();
271}
272#endif  // OS_CHROMEOS
273
274void MaximizeModeController::HandleHingeRotation(const gfx::Vector3dF& base,
275                                                 const gfx::Vector3dF& lid) {
276  static const gfx::Vector3dF hinge_vector(1.0f, 0.0f, 0.0f);
277  bool maximize_mode_engaged = IsMaximizeModeWindowManagerEnabled();
278  // Ignore the component of acceleration parallel to the hinge for the purposes
279  // of hinge angle calculation.
280  gfx::Vector3dF base_flattened(base);
281  gfx::Vector3dF lid_flattened(lid);
282  base_flattened.set_x(0.0f);
283  lid_flattened.set_x(0.0f);
284
285  // As the hinge approaches a vertical angle, the base and lid accelerometers
286  // approach the same values making any angle calculations highly inaccurate.
287  // Bail out early when it is too close.
288  if (base_flattened.Length() < kHingeAngleDetectionThreshold ||
289      lid_flattened.Length() < kHingeAngleDetectionThreshold) {
290    return;
291  }
292
293  // Compute the angle between the base and the lid.
294  float lid_angle = 180.0f - ClockwiseAngleBetweenVectorsInDegrees(
295      base_flattened, lid_flattened, hinge_vector);
296  if (lid_angle < 0.0f)
297    lid_angle += 360.0f;
298
299  bool is_angle_stable = lid_angle >= kMinStableAngle &&
300                         lid_angle <= kMaxStableAngle;
301
302  // Clear the last_lid_open_time_ for a stable reading so that there is less
303  // chance of a delay if the lid is moved from the close state to the fully
304  // open state very quickly.
305  if (is_angle_stable)
306    last_lid_open_time_ = base::TimeTicks();
307
308  // Toggle maximize mode on or off when corresponding thresholds are passed.
309  // TODO(flackr): Make MaximizeModeController own the MaximizeModeWindowManager
310  // such that observations of state changes occur after the change and shell
311  // has fewer states to track.
312  if (maximize_mode_engaged && is_angle_stable &&
313      lid_angle <= kExitMaximizeModeAngle) {
314    LeaveMaximizeMode();
315  } else if (!lid_is_closed_ && !maximize_mode_engaged &&
316             lid_angle >= kEnterMaximizeModeAngle &&
317             (is_angle_stable || !WasLidOpenedRecently())) {
318    EnterMaximizeMode();
319  }
320}
321
322void MaximizeModeController::HandleScreenRotation(const gfx::Vector3dF& lid) {
323  bool maximize_mode_engaged = IsMaximizeModeWindowManagerEnabled();
324
325  // TODO(jonross): track the updated rotation angle even when locked. So that
326  // when rotation lock is removed the accelerometer rotation can be applied
327  // without waiting for the next update.
328  if (!maximize_mode_engaged || rotation_locked_)
329    return;
330
331  DisplayManager* display_manager =
332      Shell::GetInstance()->display_manager();
333  gfx::Display::Rotation current_rotation = display_manager->GetDisplayInfo(
334      gfx::Display::InternalDisplayId()).rotation();
335
336  // After determining maximize mode state, determine if the screen should
337  // be rotated.
338  gfx::Vector3dF lid_flattened(lid.x(), lid.y(), 0.0f);
339  float lid_flattened_length = lid_flattened.Length();
340  // When the lid is close to being flat, don't change rotation as it is too
341  // sensitive to slight movements.
342  if (lid_flattened_length < kMinimumAccelerationScreenRotation)
343    return;
344
345  // The reference vector is the angle of gravity when the device is rotated
346  // clockwise by 45 degrees. Computing the angle between this vector and
347  // gravity we can easily determine the expected display rotation.
348  static const gfx::Vector3dF rotation_reference(-1.0f, -1.0f, 0.0f);
349
350  // Set the down vector to match the expected direction of gravity given the
351  // last configured rotation. This is used to enforce a stickiness that the
352  // user must overcome to rotate the display and prevents frequent rotations
353  // when holding the device near 45 degrees.
354  gfx::Vector3dF down(0.0f, 0.0f, 0.0f);
355  if (current_rotation == gfx::Display::ROTATE_0)
356    down.set_y(-1.0f);
357  else if (current_rotation == gfx::Display::ROTATE_90)
358    down.set_x(-1.0f);
359  else if (current_rotation == gfx::Display::ROTATE_180)
360    down.set_y(1.0f);
361  else
362    down.set_x(1.0f);
363
364  // Don't rotate if the screen has not passed the threshold.
365  if (AngleBetweenVectorsInDegrees(down, lid_flattened) <
366      kDisplayRotationStickyAngleDegrees) {
367    return;
368  }
369
370  float angle = ClockwiseAngleBetweenVectorsInDegrees(rotation_reference,
371      lid_flattened, gfx::Vector3dF(0.0f, 0.0f, -1.0f));
372
373  gfx::Display::Rotation new_rotation = gfx::Display::ROTATE_90;
374  if (angle < 90.0f)
375    new_rotation = gfx::Display::ROTATE_0;
376  else if (angle < 180.0f)
377    new_rotation = gfx::Display::ROTATE_270;
378  else if (angle < 270.0f)
379    new_rotation = gfx::Display::ROTATE_180;
380
381  if (new_rotation != current_rotation)
382    SetDisplayRotation(display_manager, new_rotation);
383}
384
385void MaximizeModeController::SetDisplayRotation(
386    DisplayManager* display_manager,
387    gfx::Display::Rotation rotation) {
388  base::AutoReset<bool> auto_ignore_display_configuration_updates(
389      &ignore_display_configuration_updates_, true);
390  current_rotation_ = rotation;
391  display_manager->SetDisplayRotation(gfx::Display::InternalDisplayId(),
392                                      rotation);
393}
394
395void MaximizeModeController::EnterMaximizeMode() {
396  if (IsMaximizeModeWindowManagerEnabled())
397    return;
398  DisplayManager* display_manager = Shell::GetInstance()->display_manager();
399  if (display_manager->HasInternalDisplay()) {
400    current_rotation_ = user_rotation_ = display_manager->
401        GetDisplayInfo(gfx::Display::InternalDisplayId()).rotation();
402    LoadDisplayRotationProperties();
403  }
404  EnableMaximizeModeWindowManager(true);
405#if defined(USE_X11)
406  event_blocker_.reset(new ScopedDisableInternalMouseAndKeyboardX11);
407#endif
408  Shell::GetInstance()->display_controller()->AddObserver(this);
409}
410
411void MaximizeModeController::LeaveMaximizeMode() {
412  if (!IsMaximizeModeWindowManagerEnabled())
413    return;
414  DisplayManager* display_manager = Shell::GetInstance()->display_manager();
415  if (display_manager->HasInternalDisplay()) {
416    gfx::Display::Rotation current_rotation = display_manager->
417        GetDisplayInfo(gfx::Display::InternalDisplayId()).rotation();
418    if (current_rotation != user_rotation_)
419      SetDisplayRotation(display_manager, user_rotation_);
420  }
421  if (!shutting_down_)
422    SetRotationLocked(false);
423  EnableMaximizeModeWindowManager(false);
424  event_blocker_.reset();
425  Shell::GetInstance()->display_controller()->RemoveObserver(this);
426}
427
428// Called after maximize mode has started, windows might still animate though.
429void MaximizeModeController::OnMaximizeModeStarted() {
430  RecordTouchViewStateTransition();
431}
432
433// Called after maximize mode has ended, windows might still be returning to
434// their original position.
435void MaximizeModeController::OnMaximizeModeEnded() {
436  RecordTouchViewStateTransition();
437}
438
439void MaximizeModeController::RecordTouchViewStateTransition() {
440  if (CanEnterMaximizeMode()) {
441    base::Time current_time = base::Time::Now();
442    base::TimeDelta delta = current_time - last_touchview_transition_time_;
443    if (IsMaximizeModeWindowManagerEnabled()) {
444      UMA_HISTOGRAM_LONG_TIMES("Ash.TouchView.TouchViewInactive", delta);
445      total_non_touchview_time_ += delta;
446    } else {
447      UMA_HISTOGRAM_LONG_TIMES("Ash.TouchView.TouchViewActive", delta);
448      total_touchview_time_ += delta;
449    }
450    last_touchview_transition_time_ = current_time;
451  }
452}
453
454void MaximizeModeController::LoadDisplayRotationProperties() {
455  DisplayManager* display_manager = Shell::GetInstance()->display_manager();
456  if (!display_manager->registered_internal_display_rotation_lock())
457    return;
458
459  SetDisplayRotation(display_manager,
460                     display_manager->registered_internal_display_rotation());
461  SetRotationLocked(true);
462}
463
464void MaximizeModeController::OnAppTerminating() {
465  if (CanEnterMaximizeMode()) {
466    RecordTouchViewStateTransition();
467    UMA_HISTOGRAM_CUSTOM_COUNTS("Ash.TouchView.TouchViewActiveTotal",
468        total_touchview_time_.InMinutes(),
469        1, base::TimeDelta::FromDays(7).InMinutes(), 50);
470    UMA_HISTOGRAM_CUSTOM_COUNTS("Ash.TouchView.TouchViewInactiveTotal",
471        total_non_touchview_time_.InMinutes(),
472        1, base::TimeDelta::FromDays(7).InMinutes(), 50);
473    base::TimeDelta total_runtime = total_touchview_time_ +
474        total_non_touchview_time_;
475    if (total_runtime.InSeconds() > 0) {
476      UMA_HISTOGRAM_PERCENTAGE("Ash.TouchView.TouchViewActivePercentage",
477          100 * total_touchview_time_.InSeconds() / total_runtime.InSeconds());
478    }
479  }
480  Shell::GetInstance()->display_controller()->RemoveObserver(this);
481}
482
483bool MaximizeModeController::WasLidOpenedRecently() const {
484  if (last_lid_open_time_.is_null())
485    return false;
486
487  base::TimeTicks now = tick_clock_->NowTicks();
488  DCHECK(now >= last_lid_open_time_);
489  base::TimeDelta elapsed_time = now - last_lid_open_time_;
490  return elapsed_time <= kLidRecentlyOpenedDuration;
491}
492
493void MaximizeModeController::SetTickClockForTest(
494    scoped_ptr<base::TickClock> tick_clock) {
495  DCHECK(tick_clock_);
496  tick_clock_ = tick_clock.Pass();
497}
498
499}  // namespace ash
500