blob: 041a579b3645ac264026f7927b82448a0cbffd48 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/display/screen_orientation_controller.h"
#include "ash/accelerometer/accelerometer_reader.h"
#include "ash/accelerometer/accelerometer_types.h"
#include "ash/public/cpp/app_types.h"
#include "ash/public/cpp/ash_switches.h"
#include "ash/shell.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/window_state.h"
#include "base/auto_reset.h"
#include "base/command_line.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/display/display.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/manager/managed_display_info.h"
#include "ui/gfx/geometry/size.h"
#include "ui/wm/public/activation_client.h"
namespace ash {
namespace {
// The angle which the screen has to be rotated past before the display will
// rotate to match it (i.e. 45.0f is no stickiness).
const float kDisplayRotationStickyAngleDegrees = 60.0f;
// The minimum acceleration in m/s^2 in a direction required to trigger screen
// rotation. This prevents rapid toggling of rotation when the device is near
// flat and there is very little screen aligned force on it. The value is
// effectively the sine of the rise angle required times the acceleration due
// to gravity, with the current value requiring at least a 25 degree rise.
const float kMinimumAccelerationScreenRotation = 4.2f;
OrientationLockType GetDisplayNaturalOrientation() {
if (!display::Display::HasInternalDisplay())
return OrientationLockType::kLandscape;
display::ManagedDisplayInfo info =
Shell::Get()->display_manager()->GetDisplayInfo(
display::Display::InternalDisplayId());
gfx::Size size = info.bounds_in_native().size();
return size.width() > size.height() ? OrientationLockType::kLandscape
: OrientationLockType::kPortrait;
}
OrientationLockType RotationToOrientation(OrientationLockType natural,
display::Display::Rotation rotation) {
if (natural == OrientationLockType::kLandscape) {
// To be consistent with Android, the rgotation of the primary portrait
// on naturally landscape device is 270.
switch (rotation) {
case display::Display::ROTATE_0:
return OrientationLockType::kLandscapePrimary;
case display::Display::ROTATE_90:
return OrientationLockType::kPortraitSecondary;
case display::Display::ROTATE_180:
return OrientationLockType::kLandscapeSecondary;
case display::Display::ROTATE_270:
return OrientationLockType::kPortraitPrimary;
}
} else { // Natural portrait
switch (rotation) {
case display::Display::ROTATE_0:
return OrientationLockType::kPortraitPrimary;
case display::Display::ROTATE_90:
return OrientationLockType::kLandscapePrimary;
case display::Display::ROTATE_180:
return OrientationLockType::kPortraitSecondary;
case display::Display::ROTATE_270:
return OrientationLockType::kLandscapeSecondary;
}
}
NOTREACHED();
return OrientationLockType::kAny;
}
// Returns the rotation that matches the orientation type.
// Returns ROTATE_0 if the given orientation is ANY, which is used
// to indicate that user didn't lock orientation.
display::Display::Rotation OrientationToRotation(
OrientationLockType natural,
OrientationLockType orientation) {
if (orientation == OrientationLockType::kAny)
return display::Display::ROTATE_0;
if (natural == OrientationLockType::kLandscape) {
// To be consistent with Android, the rotation of the primary portrait
// on naturally landscape device is 270.
switch (orientation) {
case OrientationLockType::kLandscapePrimary:
return display::Display::ROTATE_0;
case OrientationLockType::kPortraitPrimary:
return display::Display::ROTATE_270;
case OrientationLockType::kLandscapeSecondary:
return display::Display::ROTATE_180;
case OrientationLockType::kPortraitSecondary:
return display::Display::ROTATE_90;
default:
break;
}
} else { // Natural portrait
switch (orientation) {
case OrientationLockType::kPortraitPrimary:
return display::Display::ROTATE_0;
case OrientationLockType::kLandscapePrimary:
return display::Display::ROTATE_90;
case OrientationLockType::kPortraitSecondary:
return display::Display::ROTATE_180;
case OrientationLockType::kLandscapeSecondary:
return display::Display::ROTATE_270;
default:
break;
}
}
NOTREACHED() << static_cast<int>(orientation);
return display::Display::ROTATE_0;
}
// Returns the locked orientation that matches the application
// requested orientation, or the application orientation itself
// if it didn't match.
OrientationLockType ResolveOrientationLock(OrientationLockType app_requested,
OrientationLockType lock) {
if (app_requested == OrientationLockType::kAny ||
(app_requested == OrientationLockType::kLandscape &&
(lock == OrientationLockType::kLandscapePrimary ||
lock == OrientationLockType::kLandscapeSecondary)) ||
(app_requested == OrientationLockType::kPortrait &&
(lock == OrientationLockType::kPortraitPrimary ||
lock == OrientationLockType::kPortraitSecondary))) {
return lock;
}
return app_requested;
}
} // namespace
bool IsPrimaryOrientation(OrientationLockType type) {
return type == OrientationLockType::kLandscapePrimary ||
type == OrientationLockType::kPortraitPrimary;
}
bool IsLandscapeOrientation(OrientationLockType type) {
return type == OrientationLockType::kLandscape ||
type == OrientationLockType::kLandscapePrimary ||
type == OrientationLockType::kLandscapeSecondary;
}
bool IsPortraitOrientation(OrientationLockType type) {
return type == OrientationLockType::kPortrait ||
type == OrientationLockType::kPortraitPrimary ||
type == OrientationLockType::kPortraitSecondary;
}
OrientationLockType GetCurrentScreenOrientation() {
// ScreenOrientationController might be nullptr during shutdown.
// TODO(xdai|sammiequon): See if we can reorder so that users of the function
// (split_view_controller) get shutddown before screen orientation controller.
if (!Shell::Get()->screen_orientation_controller())
return OrientationLockType::kAny;
return Shell::Get()->screen_orientation_controller()->GetCurrentOrientation();
}
bool IsCurrentScreenOrientationLandscape() {
return IsLandscapeOrientation(GetCurrentScreenOrientation());
}
bool IsCurrentScreenOrientationPrimary() {
return IsPrimaryOrientation(GetCurrentScreenOrientation());
}
std::ostream& operator<<(std::ostream& out, const OrientationLockType& lock) {
switch (lock) {
case OrientationLockType::kAny:
out << "any";
break;
case OrientationLockType::kNatural:
out << "natural";
break;
case OrientationLockType::kCurrent:
out << "current";
break;
case OrientationLockType::kPortrait:
out << "portrait";
break;
case OrientationLockType::kLandscape:
out << "landscape";
break;
case OrientationLockType::kPortraitPrimary:
out << "portrait-primary";
break;
case OrientationLockType::kPortraitSecondary:
out << "portrait-secondary";
break;
case OrientationLockType::kLandscapePrimary:
out << "landscape-primary";
break;
case OrientationLockType::kLandscapeSecondary:
out << "landscape-secondary";
break;
}
return out;
}
ScreenOrientationController::ScreenOrientationController()
: natural_orientation_(GetDisplayNaturalOrientation()),
ignore_display_configuration_updates_(false),
rotation_locked_(false),
rotation_locked_orientation_(OrientationLockType::kAny),
user_rotation_(display::Display::ROTATE_0),
current_rotation_(display::Display::ROTATE_0) {
Shell::Get()->tablet_mode_controller()->AddObserver(this);
}
ScreenOrientationController::~ScreenOrientationController() {
Shell::Get()->tablet_mode_controller()->RemoveObserver(this);
AccelerometerReader::GetInstance()->RemoveObserver(this);
Shell::Get()->window_tree_host_manager()->RemoveObserver(this);
Shell::Get()->activation_client()->RemoveObserver(this);
for (auto& windows : lock_info_map_)
windows.first->RemoveObserver(this);
}
void ScreenOrientationController::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void ScreenOrientationController::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void ScreenOrientationController::LockOrientationForWindow(
aura::Window* requesting_window,
OrientationLockType orientation_lock) {
if (!requesting_window->HasObserver(this))
requesting_window->AddObserver(this);
auto iter = lock_info_map_.find(requesting_window);
if (iter != lock_info_map_.end()) {
if (orientation_lock == OrientationLockType::kCurrent) {
// If the app previously requested an orientation,
// disable the sensor when that orientation is locked.
iter->second.lock_completion_behavior =
LockCompletionBehavior::DisableSensor;
} else {
iter->second.orientation_lock = orientation_lock;
iter->second.lock_completion_behavior = LockCompletionBehavior::None;
}
} else {
lock_info_map_.emplace(requesting_window, LockInfo(orientation_lock));
}
ApplyLockForActiveWindow();
}
void ScreenOrientationController::UnlockOrientationForWindow(
aura::Window* window) {
lock_info_map_.erase(window);
window->RemoveObserver(this);
ApplyLockForActiveWindow();
}
void ScreenOrientationController::UnlockAll() {
SetRotationLockedInternal(false);
if (user_rotation_ != current_rotation_) {
SetDisplayRotation(user_rotation_,
display::Display::RotationSource::ACCELEROMETER,
DisplayConfigurationController::ANIMATION_SYNC);
}
}
bool ScreenOrientationController::ScreenOrientationProviderSupported() const {
return Shell::Get()
->tablet_mode_controller()
->IsTabletModeWindowManagerEnabled();
}
bool ScreenOrientationController::IsUserLockedOrientationPortrait() {
switch (user_locked_orientation_) {
case OrientationLockType::kPortraitPrimary:
case OrientationLockType::kPortraitSecondary:
case OrientationLockType::kPortrait:
return true;
default:
return false;
}
}
void ScreenOrientationController::ToggleUserRotationLock() {
if (!display::Display::HasInternalDisplay())
return;
if (user_rotation_locked()) {
SetLockToOrientation(OrientationLockType::kAny);
} else {
display::Display::Rotation current_rotation =
Shell::Get()
->display_manager()
->GetDisplayInfo(display::Display::InternalDisplayId())
.GetActiveRotation();
SetLockToRotation(current_rotation);
}
}
void ScreenOrientationController::SetLockToRotation(
display::Display::Rotation rotation) {
if (!display::Display::HasInternalDisplay())
return;
SetLockToOrientation(RotationToOrientation(natural_orientation_, rotation));
}
OrientationLockType ScreenOrientationController::GetCurrentOrientation() const {
return RotationToOrientation(natural_orientation_, current_rotation_);
}
void ScreenOrientationController::OnWindowActivated(
::wm::ActivationChangeObserver::ActivationReason reason,
aura::Window* gained_active,
aura::Window* lost_active) {
ApplyLockForActiveWindow();
}
void ScreenOrientationController::OnWindowDestroying(aura::Window* window) {
UnlockOrientationForWindow(window);
}
// Currently contents::WebContents will only be able to lock rotation while
// fullscreen. In this state a user cannot click on the tab strip to change. If
// this becomes supported for non-fullscreen tabs then the following interferes
// with TabDragController. OnWindowVisibilityChanged is called between a mouse
// down and mouse up. The rotation this triggers leads to a coordinate space
// change in the middle of an event. Causes the tab to separate from the tab
// strip.
void ScreenOrientationController::OnWindowVisibilityChanged(
aura::Window* window,
bool visible) {
if (lock_info_map_.find(window) == lock_info_map_.end())
return;
ApplyLockForActiveWindow();
}
void ScreenOrientationController::OnAccelerometerUpdated(
scoped_refptr<const AccelerometerUpdate> update) {
if (rotation_locked_ && !CanRotateInLockedState())
return;
if (!update->has(ACCELEROMETER_SOURCE_SCREEN))
return;
// Ignore the reading if it appears unstable. The reading is considered
// unstable if it deviates too much from gravity
if (update->IsReadingStable(ACCELEROMETER_SOURCE_SCREEN))
HandleScreenRotation(update->get(ACCELEROMETER_SOURCE_SCREEN));
}
void ScreenOrientationController::OnDisplayConfigurationChanged() {
if (ignore_display_configuration_updates_)
return;
if (!display::Display::HasInternalDisplay())
return;
if (!Shell::Get()->display_manager()->IsActiveDisplayId(
display::Display::InternalDisplayId())) {
return;
}
// TODO(oshima): remove current_rotation_ and always use the target rotation.
current_rotation_ =
Shell::Get()->display_configuration_controller()->GetTargetRotation(
display::Display::InternalDisplayId());
}
void ScreenOrientationController::OnTabletModeStarted() {
Shell* shell = Shell::Get();
// Do not exit early, as the internal display can be determined after Maximize
// Mode has started. (chrome-os-partner:38796)
// Always start observing.
if (display::Display::HasInternalDisplay()) {
current_rotation_ = user_rotation_ =
shell->display_configuration_controller()->GetTargetRotation(
display::Display::InternalDisplayId());
}
if (!rotation_locked_)
LoadDisplayRotationProperties();
AccelerometerReader::GetInstance()->AddObserver(this);
shell->window_tree_host_manager()->AddObserver(this);
Shell::Get()->activation_client()->AddObserver(this);
if (!display::Display::HasInternalDisplay())
return;
ApplyLockForActiveWindow();
for (auto& observer : observers_)
observer.OnUserRotationLockChanged();
}
void ScreenOrientationController::OnTabletModeEnding() {
AccelerometerReader::GetInstance()->RemoveObserver(this);
Shell::Get()->window_tree_host_manager()->RemoveObserver(this);
Shell::Get()->activation_client()->RemoveObserver(this);
if (!display::Display::HasInternalDisplay())
return;
// TODO(oshima): Remove if when current_rotation_ is removed.
if (current_rotation_ != user_rotation_) {
SetDisplayRotation(user_rotation_,
display::Display::RotationSource::ACCELEROMETER,
DisplayConfigurationController::ANIMATION_SYNC);
}
for (auto& observer : observers_)
observer.OnUserRotationLockChanged();
}
void ScreenOrientationController::OnTabletModeEnded() {
UnlockAll();
}
void ScreenOrientationController::SetDisplayRotation(
display::Display::Rotation rotation,
display::Display::RotationSource source,
DisplayConfigurationController::RotationAnimation mode) {
if (!display::Display::HasInternalDisplay())
return;
current_rotation_ = rotation;
base::AutoReset<bool> auto_ignore_display_configuration_updates(
&ignore_display_configuration_updates_, true);
Shell::Get()->display_configuration_controller()->SetDisplayRotation(
display::Display::InternalDisplayId(), rotation, source, mode);
}
void ScreenOrientationController::SetRotationLockedInternal(
bool rotation_locked) {
if (rotation_locked_ == rotation_locked)
return;
rotation_locked_ = rotation_locked;
if (!rotation_locked_)
rotation_locked_orientation_ = OrientationLockType::kAny;
}
void ScreenOrientationController::SetLockToOrientation(
OrientationLockType orientation) {
user_locked_orientation_ = orientation;
base::AutoReset<bool> auto_ignore_display_configuration_updates(
&ignore_display_configuration_updates_, true);
Shell::Get()->display_manager()->RegisterDisplayRotationProperties(
user_rotation_locked(),
OrientationToRotation(natural_orientation_, user_locked_orientation_));
ApplyLockForActiveWindow();
for (auto& observer : observers_)
observer.OnUserRotationLockChanged();
}
void ScreenOrientationController::LockRotation(
display::Display::Rotation rotation,
display::Display::RotationSource source) {
SetRotationLockedInternal(true);
SetDisplayRotation(rotation, source);
}
void ScreenOrientationController::LockRotationToOrientation(
OrientationLockType lock_orientation) {
rotation_locked_orientation_ = lock_orientation;
switch (lock_orientation) {
case OrientationLockType::kAny:
SetRotationLockedInternal(false);
break;
case OrientationLockType::kLandscape:
case OrientationLockType::kPortrait:
LockToRotationMatchingOrientation(lock_orientation);
break;
case OrientationLockType::kLandscapePrimary:
case OrientationLockType::kLandscapeSecondary:
case OrientationLockType::kPortraitPrimary:
case OrientationLockType::kPortraitSecondary:
LockRotation(
OrientationToRotation(natural_orientation_, lock_orientation),
display::Display::RotationSource::ACTIVE);
break;
case OrientationLockType::kNatural:
LockRotation(display::Display::ROTATE_0,
display::Display::RotationSource::ACTIVE);
break;
default:
NOTREACHED();
break;
}
}
void ScreenOrientationController::LockToRotationMatchingOrientation(
OrientationLockType lock_orientation) {
if (!display::Display::HasInternalDisplay())
return;
display::Display::Rotation rotation =
Shell::Get()
->display_manager()
->GetDisplayInfo(display::Display::InternalDisplayId())
.GetActiveRotation();
if (natural_orientation_ == lock_orientation) {
if (rotation == display::Display::ROTATE_0 ||
rotation == display::Display::ROTATE_180) {
SetRotationLockedInternal(true);
} else {
LockRotation(display::Display::ROTATE_0,
display::Display::RotationSource::ACTIVE);
}
} else {
if (rotation == display::Display::ROTATE_90 ||
rotation == display::Display::ROTATE_270) {
SetRotationLockedInternal(true);
} else {
// Rotate to the default rotation of the requested orientation.
display::Display::Rotation default_rotation =
natural_orientation_ == OrientationLockType::kLandscape
? display::Display::ROTATE_270 // portrait in landscape device.
: display::Display::ROTATE_90; // landscape in portrait device.
LockRotation(default_rotation, display::Display::RotationSource::ACTIVE);
}
}
}
void ScreenOrientationController::HandleScreenRotation(
const AccelerometerReading& lid) {
gfx::Vector3dF lid_flattened(lid.x, lid.y, 0.0f);
float lid_flattened_length = lid_flattened.Length();
// When the lid is close to being flat, don't change rotation as it is too
// sensitive to slight movements.
if (lid_flattened_length < kMinimumAccelerationScreenRotation)
return;
// The reference vector is the angle of gravity when the device is rotated
// clockwise by 45 degrees. Computing the angle between this vector and
// gravity we can easily determine the expected display rotation.
static const gfx::Vector3dF rotation_reference(-1.0f, 1.0f, 0.0f);
// Set the down vector to match the expected direction of gravity given the
// last configured rotation. This is used to enforce a stickiness that the
// user must overcome to rotate the display and prevents frequent rotations
// when holding the device near 45 degrees.
gfx::Vector3dF down(0.0f, 0.0f, 0.0f);
if (current_rotation_ == display::Display::ROTATE_0)
down.set_y(1.0f);
else if (current_rotation_ == display::Display::ROTATE_90)
down.set_x(1.0f);
else if (current_rotation_ == display::Display::ROTATE_180)
down.set_y(-1.0f);
else
down.set_x(-1.0f);
// Don't rotate if the screen has not passed the threshold.
if (gfx::AngleBetweenVectorsInDegrees(down, lid_flattened) <
kDisplayRotationStickyAngleDegrees) {
return;
}
float angle = gfx::ClockwiseAngleBetweenVectorsInDegrees(
rotation_reference, lid_flattened, gfx::Vector3dF(0.0f, 0.0f, 1.0f));
display::Display::Rotation new_rotation = display::Display::ROTATE_270;
if (angle < 90.0f)
new_rotation = display::Display::ROTATE_0;
else if (angle < 180.0f)
new_rotation = display::Display::ROTATE_90;
else if (angle < 270.0f)
new_rotation = display::Display::ROTATE_180;
if (new_rotation != current_rotation_ &&
IsRotationAllowedInLockedState(new_rotation)) {
SetDisplayRotation(new_rotation,
display::Display::RotationSource::ACCELEROMETER);
}
}
void ScreenOrientationController::LoadDisplayRotationProperties() {
display::DisplayManager* display_manager = Shell::Get()->display_manager();
if (!display_manager->registered_internal_display_rotation_lock())
return;
user_locked_orientation_ = RotationToOrientation(
natural_orientation_,
display_manager->registered_internal_display_rotation());
}
void ScreenOrientationController::ApplyLockForActiveWindow() {
if (!ScreenOrientationProviderSupported())
return;
MruWindowTracker::WindowList mru_windows(
Shell::Get()->mru_window_tracker()->BuildMruWindowList());
for (auto* window : mru_windows) {
if (!window->TargetVisibility())
continue;
for (auto& pair : lock_info_map_) {
if (pair.first->TargetVisibility() && window->Contains(pair.first)) {
if (pair.second.orientation_lock == OrientationLockType::kCurrent) {
// If the app requested "current" without previously
// specifying an orientation, use the current rotation.
pair.second.orientation_lock =
RotationToOrientation(natural_orientation_, current_rotation_);
LockRotationToOrientation(pair.second.orientation_lock);
} else {
const auto orientation_lock = ResolveOrientationLock(
pair.second.orientation_lock, user_locked_orientation_);
LockRotationToOrientation(orientation_lock);
if (pair.second.lock_completion_behavior ==
LockCompletionBehavior::DisableSensor) {
pair.second.lock_completion_behavior = LockCompletionBehavior::None;
pair.second.orientation_lock = orientation_lock;
}
}
return;
}
}
// The default orientation for all chrome browser/apps windows is
// ANY, so use the user_locked_orientation_;
if (window->TargetVisibility() &&
static_cast<AppType>(window->GetProperty(aura::client::kAppType)) !=
AppType::OTHERS) {
LockRotationToOrientation(user_locked_orientation_);
return;
}
}
LockRotationToOrientation(user_locked_orientation_);
}
bool ScreenOrientationController::IsRotationAllowedInLockedState(
display::Display::Rotation rotation) {
if (!rotation_locked_)
return true;
if (!CanRotateInLockedState())
return false;
if (natural_orientation_ == rotation_locked_orientation_) {
return rotation == display::Display::ROTATE_0 ||
rotation == display::Display::ROTATE_180;
} else {
return rotation == display::Display::ROTATE_90 ||
rotation == display::Display::ROTATE_270;
}
return false;
}
bool ScreenOrientationController::CanRotateInLockedState() {
return rotation_locked_orientation_ == OrientationLockType::kLandscape ||
rotation_locked_orientation_ == OrientationLockType::kPortrait;
}
void ScreenOrientationController::UpdateNaturalOrientationForTest() {
natural_orientation_ = GetDisplayNaturalOrientation();
}
} // namespace ash