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