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