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