// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include <algorithm>
#include <string>
#include <utility>
#include "ash/accessibility/accessibility_controller.h"
#include "ash/app_menu/menu_util.h"
#include "ash/constants/ash_switches.h"
#include "ash/login_status.h"
#include "ash/public/cpp/metrics_util.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/tablet_mode_observer.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/utility/layer_util.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/tablet_mode/internal_input_devices_event_blocker.h"
#include "ash/wm/tablet_mode/tablet_mode_window_manager.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/metrics/histogram.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/notreached.h"
#include "base/system/sys_info.h"
#include "base/time/default_tick_clock.h"
#include "base/time/tick_clock.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window_observer.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_sequence.h"
#include "ui/compositor/layer_animator.h"
#include "ui/display/display.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/screen.h"
#include "ui/display/tablet_state.h"
#include "ui/display/util/display_util.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/events/devices/input_device.h"
#include "ui/events/event.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/cursor_manager.h"
#include "ui/wm/core/window_util.h"
#undef ENABLED_VLOG_LEVEL
#define ENABLED_VLOG_LEVEL 1
namespace ash {
BASE_FEATURE(kBlockUiTabletModeInKiosk,
"BlockUiTabletModeInKiosk",
base::FEATURE_ENABLED_BY_DEFAULT);
namespace {
// The hinge angle at which to enter tablet mode.
constexpr float kEnterTabletModeAngle = 200.0f;
// The angle at which to exit tablet mode, this is specifically less than the
// angle to enter tablet mode to prevent rapid toggling when near the angle.
constexpr float kExitTabletModeAngle = 160.0f;
// Defines a range for which accelerometer readings are considered accurate.
// When the lid is near open (or near closed) the accelerometer readings may be
// inaccurate and a lid that is fully open may appear to be near closed (and
// vice versa).
constexpr float kMinStableAngle = 20.0f;
constexpr float kMaxStableAngle = 340.0f;
// The time duration to consider an unstable lid angle to be valid. This is used
// to prevent entering tablet mode if an erroneous accelerometer reading makes
// the lid appear to be fully open when the user is opening the lid from a
// closed position or is closing the lid from an opened position.
constexpr base::TimeDelta kUnstableLidAngleDuration = base::Seconds(2);
// When the device approaches vertical orientation (i.e. portrait orientation)
// the accelerometers for the base and lid approach the same values (i.e.
// gravity pointing in the direction of the hinge). When this happens abrupt
// small acceleration perpendicular to the hinge can lead to incorrect hinge
// angle calculations. To prevent this the accelerometer updates will be
// smoothed over time in order to reduce this noise.
// This is the minimum acceleration parallel to the hinge under which to begin
// smoothing in m/s^2.
constexpr float kHingeVerticalSmoothingStart = 7.0f;
// This is the maximum acceleration parallel to the hinge under which smoothing
// will incorporate new acceleration values, in m/s^2.
constexpr float kHingeVerticalSmoothingMaximum = 8.7f;
// The maximum deviation between the magnitude of the two accelerometers under
// which to detect hinge angle in m/s^2. These accelerometers are attached to
// the same physical device and so should be under the same acceleration.
constexpr float kNoisyMagnitudeDeviation = 1.0f;
// Interval between calls to RecordLidAngle().
constexpr base::TimeDelta kRecordLidAngleInterval = base::Hours(1);
// Time that should wait to reset |occlusion_tracker_pauser_| on
// entering/exiting tablet mode.
constexpr base::TimeDelta kOcclusionTrackerTimeout = base::Seconds(1);
// Histogram names for recording animation smoothness when entering or exiting
// tablet mode.
constexpr char kTabletModeEnterHistogram[] =
"Ash.TabletMode.AnimationSmoothness.Enter";
constexpr char kTabletModeExitHistogram[] =
"Ash.TabletMode.AnimationSmoothness.Exit";
// Set to false for tests so tablet mode can be changed synchronously.
bool use_screenshot_for_test = true;
// The angle between AccelerometerReadings are considered stable if
// their magnitudes do not differ greatly. This returns false if the deviation
// between the screen and keyboard accelerometers is too high.
bool IsAngleBetweenAccelerometerReadingsStable(
const AccelerometerUpdate& update) {
return std::abs(
update.GetVector(ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD).Length() -
update.GetVector(ACCELEROMETER_SOURCE_SCREEN).Length()) <=
kNoisyMagnitudeDeviation;
}
// Returns the UiMode given by the force-table-mode command line.
TabletModeController::UiMode GetUiMode() {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(switches::kAshUiMode)) {
std::string switch_value =
command_line->GetSwitchValueASCII(switches::kAshUiMode);
if (switch_value == switches::kAshUiModeClamshell) {
return TabletModeController::UiMode::kClamshell;
}
if (switch_value == switches::kAshUiModeTablet) {
return TabletModeController::UiMode::kTabletMode;
}
}
return TabletModeController::UiMode::kNone;
}
// Returns true if the device has an active internal display.
bool HasActiveInternalDisplay() {
if (!display::HasInternalDisplay()) {
return false;
}
display::DisplayManager* display_manager = Shell::Get()->display_manager();
return display_manager->IsActiveDisplayId(
display::Display::InternalDisplayId()) ||
display_manager->IsInUnifiedMode();
}
// Returns true if |sequence| has the same properties as the ones we care about
// for the tablet transition animation.
bool ShouldObserveSequence(ui::LayerAnimationSequence* sequence) {
DCHECK(sequence);
return sequence->properties() &
TabletModeController::GetObservedTabletTransitionProperty();
}
// Check if there is any external and internal pointing device in
// |input_devices|.
template <typename PointingDeviceType>
void CheckHasPointingDevices(
const std::vector<PointingDeviceType>& input_devices,
BluetoothDevicesObserver* bluetooth_device_observer,
bool& out_has_external_pointing_device,
bool& out_has_internal_pointing_device) {
static_assert(std::is_base_of<ui::InputDevice, PointingDeviceType>::value);
for (const PointingDeviceType& input_device : input_devices) {
if (input_device.type == ui::INPUT_DEVICE_INTERNAL) {
out_has_internal_pointing_device = true;
} else if (input_device.type == ui::INPUT_DEVICE_USB ||
(input_device.type == ui::INPUT_DEVICE_BLUETOOTH &&
bluetooth_device_observer->IsConnectedBluetoothDevice(
input_device))) {
out_has_external_pointing_device = true;
}
if (out_has_external_pointing_device && out_has_internal_pointing_device) {
return;
}
}
}
// The default behavior in Clamshell mode.
constexpr TabletModeController::TabletModeBehavior kDefault{};
// Defines the behavior of the tablet mode when enabled by sensor.
constexpr TabletModeController::TabletModeBehavior kOnBySensor{
.block_internal_input_device = true,
};
// Defines the behavior that sticks to tablet mode. Used to implement the
// --force-tablet-mode=touch_view flag.
constexpr TabletModeController::TabletModeBehavior kLockInTabletMode{
.use_sensor = false,
.observe_display_events = false,
.observe_pointer_device_events = false,
.always_show_overview_button = true,
};
// Defines the behavior that sticks to tablet mode. Used to implement the
// --force-tablet-mode=clamshell flag.
constexpr TabletModeController::TabletModeBehavior kLockInClamshellMode{
.use_sensor = false,
.observe_display_events = false,
.observe_pointer_device_events = false,
};
// Defines the behavior used for testing. It prevents the device from
// switching the mode due to sensor events during the test.
constexpr TabletModeController::TabletModeBehavior kOnForTest{
.use_sensor = false,
.block_internal_input_device = true,
.force_physical_tablet_state =
TabletModeController::ForcePhysicalTabletState::kForceTabletMode,
};
// Used for the testing API to forcibly enter into the tablet mode. It should
// not observe hardware events as tests want to stick with the tablet mode, and
// it should not block internal keyboard as some tests may want to use keyboard
// events in the tablet mode.
constexpr TabletModeController::TabletModeBehavior kOnForAutotest{
.use_sensor = false,
.observe_display_events = false,
.observe_pointer_device_events = false,
.force_physical_tablet_state =
TabletModeController::ForcePhysicalTabletState::kForceTabletMode,
};
// Used for the testing API to forcibly exit from the tablet mode.
constexpr TabletModeController::TabletModeBehavior kOffForAutotest{
.use_sensor = false,
.observe_display_events = false,
.observe_pointer_device_events = false,
.force_physical_tablet_state =
TabletModeController::ForcePhysicalTabletState::kForceClamshellMode,
};
// Used for development purpose (currently debug shortcut shift-ctrl-alt). This
// ignores the sensor but allows to switch upon docked mode and external
// pointing device. It also forces to show the overview button.
constexpr TabletModeController::TabletModeBehavior kOnForDev{
.use_sensor = false,
.always_show_overview_button = true,
.force_physical_tablet_state =
TabletModeController::ForcePhysicalTabletState::kForceTabletMode,
};
using LidState = chromeos::PowerManagerClient::LidState;
using TabletMode = chromeos::PowerManagerClient::TabletMode;
const char* ToString(LidState lid_state) {
switch (lid_state) {
case LidState::OPEN:
return "Open";
case LidState::CLOSED:
return "Closed";
case LidState::NOT_PRESENT:
return "Not present";
}
}
const char* ToString(TabletMode tablet_mode) {
switch (tablet_mode) {
case TabletMode::ON:
return "On";
case TabletMode::OFF:
return "Off";
case TabletMode::UNSUPPORTED:
return "Unsupported";
}
}
void ReportTrasitionSmoothness(bool enter_tablet_mode, int smoothness) {
if (enter_tablet_mode) {
UMA_HISTOGRAM_PERCENTAGE(kTabletModeEnterHistogram, smoothness);
} else {
UMA_HISTOGRAM_PERCENTAGE(kTabletModeExitHistogram, smoothness);
}
}
bool ShouldBlockUiTabletModeInKiosk() {
return base::FeatureList::IsEnabled(kBlockUiTabletModeInKiosk);
}
} // namespace
// An observer that observes the destruction of the |window_| and executes the
// callback. Used to run cleanup when the window is destroyed in the middle of
// certain operation.
class TabletModeController::DestroyObserver : public aura::WindowObserver {
public:
DestroyObserver(aura::Window* window, base::OnceCallback<void(void)> callback)
: window_(window), callback_(std::move(callback)) {
window_->AddObserver(this);
}
~DestroyObserver() override {
if (window_) {
window_->RemoveObserver(this);
}
}
// aura::WindowObserver:
void OnWindowDestroying(aura::Window* window) override {
DCHECK_EQ(window_, window);
window_->RemoveObserver(this);
window_ = nullptr;
std::move(callback_).Run();
}
aura::Window* window() { return window_; }
private:
raw_ptr<aura::Window> window_;
base::OnceCallback<void(void)> callback_;
};
// Used to hide the shelf and float containers while screenshot for tablet mode
// animation is taken.
class TabletModeController::ScopedContainerHider {
public:
explicit ScopedContainerHider(aura::Window* root_window)
: root_window_(root_window) {
DCHECK(root_window->IsRootWindow());
ui::Layer* screen_animation_container_layer =
root_window->GetChildById(kShellWindowId_ScreenAnimationContainer)
->layer();
for (int id :
{kShellWindowId_FloatContainer, kShellWindowId_ShelfContainer}) {
aura::Window* container = root_window->GetChildById(id);
std::unique_ptr<ui::LayerTreeOwner> phantom_layer =
wm::RecreateLayers(container);
ui::Layer* root = phantom_layer->root();
root_window->layer()->Add(root);
root_window->layer()->StackAbove(root, screen_animation_container_layer);
phantom_layers_.push_back(std::move(phantom_layer));
container->layer()->SetOpacity(0.0f);
}
}
ScopedContainerHider(const ScopedContainerHider&) = delete;
ScopedContainerHider& operator=(const ScopedContainerHider&) = delete;
~ScopedContainerHider() {
// Cancel if the root window is deleted while taking a screenshot.
if (!base::Contains(Shell::GetAllRootWindows(), root_window_)) {
return;
}
for (int id :
{kShellWindowId_FloatContainer, kShellWindowId_ShelfContainer}) {
root_window_->GetChildById(id)->layer()->SetOpacity(1.0f);
}
}
private:
const raw_ptr<aura::Window> root_window_;
// The layer that holds the clone of shelf and float layers while the
// originals are hidden.
std::vector<std::unique_ptr<ui::LayerTreeOwner>> phantom_layers_;
};
constexpr char TabletModeController::kLidAngleHistogramName[];
constexpr char TabletModeController::kTabletInactiveTimeHistogramName[];
constexpr char TabletModeController::kTabletActiveTimeHistogramName[];
////////////////////////////////////////////////////////////////////////////////
// TabletModeController, public:
// static
void TabletModeController::SetUseScreenshotForTest(bool use_screenshot) {
use_screenshot_for_test = use_screenshot;
}
// static
ui::LayerAnimationElement::AnimatableProperty
TabletModeController::GetObservedTabletTransitionProperty() {
return ui::LayerAnimationElement::TRANSFORM;
}
TabletModeController::TabletModeController()
: event_blocker_(std::make_unique<InternalInputDevicesEventBlocker>()),
tick_clock_(base::DefaultTickClock::GetInstance()) {
Shell::Get()->AddShellObserver(this);
base::RecordAction(base::UserMetricsAction("Touchview_Initially_Disabled"));
// TODO(jonross): Do not create TabletModeController if the flag is
// unavailable. This will require refactoring
// InTabletMode to check for the existence of the
// controller.
if (IsBoardTypeMarkedAsTabletCapable()) {
Shell::Get()->display_manager()->AddDisplayManagerObserver(this);
AccelerometerReader::GetInstance()->AddObserver(this);
ui::DeviceDataManager::GetInstance()->AddObserver(this);
bluetooth_devices_observer_ =
std::make_unique<BluetoothDevicesObserver>(base::BindRepeating(
&TabletModeController::OnBluetoothAdapterOrDeviceChanged,
base::Unretained(this)));
}
chromeos::PowerManagerClient* power_manager_client =
chromeos::PowerManagerClient::Get();
power_manager_client->AddObserver(this);
}
TabletModeController::~TabletModeController() {
DCHECK(!tablet_mode_window_manager_);
}
void TabletModeController::Shutdown() {
// Stop observing any animations and delete any pending screenshots.
StopObservingAnimation(/*record_stats=*/false, /*delete_screenshot=*/true);
if (tablet_mode_window_manager_) {
tablet_mode_window_manager_->Shutdown(
TabletModeWindowManager::ShutdownReason::kSystemShutdown);
}
tablet_mode_window_manager_.reset();
UMA_HISTOGRAM_COUNTS_1000("Tablet.AppWindowDrag.CountOfPerUserSession",
app_window_drag_count_);
UMA_HISTOGRAM_COUNTS_1000(
"Tablet.AppWindowDrag.InSplitView.CountOfPerUserSession",
app_window_drag_in_splitview_count_);
UMA_HISTOGRAM_COUNTS_1000("Tablet.TabDrag.CountOfPerUserSession",
tab_drag_count_);
UMA_HISTOGRAM_COUNTS_1000("Tablet.TabDrag.InSplitView.CountOfPerUserSession",
tab_drag_in_splitview_count_);
Shell::Get()->RemoveShellObserver(this);
if (IsBoardTypeMarkedAsTabletCapable()) {
Shell::Get()->display_manager()->RemoveDisplayManagerObserver(this);
AccelerometerReader::GetInstance()->RemoveObserver(this);
ui::DeviceDataManager::GetInstance()->RemoveObserver(this);
}
chromeos::PowerManagerClient::Get()->RemoveObserver(this);
for (auto& observer : tablet_mode_observers_) {
observer.OnTabletControllerDestroyed();
}
}
void TabletModeController::AddWindow(aura::Window* window) {
if (tablet_mode_window_manager_) {
tablet_mode_window_manager_->AddWindow(window);
}
}
bool TabletModeController::ShouldAutoHideTitlebars(views::Widget* widget) {
DCHECK(widget);
if (!display::Screen::GetScreen()->InTabletMode()) {
return false;
}
return widget->IsMaximized() ||
(WindowState::Get(widget->GetNativeWindow()) &&
WindowState::Get(widget->GetNativeWindow())->IsSnapped());
}
bool TabletModeController::AreInternalInputDeviceEventsBlocked() const {
return event_blocker_->should_be_blocked();
}
bool TabletModeController::TriggerRecordLidAngleTimerForTesting() {
if (!record_lid_angle_timer_.IsRunning()) {
return false;
}
record_lid_angle_timer_.user_task().Run();
return true;
}
void TabletModeController::MaybeObserveBoundsAnimation(aura::Window* window) {
StopObservingAnimation(/*record_stats=*/false, /*delete_screenshot=*/false);
if (!display::IsTabletStateChanging(
display::Screen::GetScreen()->GetTabletState())) {
return;
}
destroy_observer_ = std::make_unique<DestroyObserver>(
window, base::BindOnce(&TabletModeController::StopObservingAnimation,
weak_factory_.GetWeakPtr(),
/*record_stats=*/false,
/*delete_screenshot=*/true));
animating_layer_ = window->layer();
animating_layer_->GetAnimator()->AddObserver(this);
animating_layer_->AddObserver(this);
}
void TabletModeController::StopObservingAnimation(bool record_stats,
bool delete_screenshot) {
StopObserving();
ResetDestroyObserver();
if (animating_layer_) {
animating_layer_->GetAnimator()->StopAnimating();
// If the observed layer is part of a cross fade animation, stopping the
// animation will end up destroying the layer.
if (animating_layer_) {
animating_layer_->RemoveObserver(this);
animating_layer_->GetAnimator()->RemoveObserver(this);
animating_layer_ = nullptr;
}
}
if (record_stats && transition_tracker_) {
transition_tracker_->Stop();
}
transition_tracker_.reset();
// Stop other animations (STEP_END), then update the tablet mode ui.
if (tablet_mode_window_manager_ && delete_screenshot) {
tablet_mode_window_manager_->StopWindowAnimations();
}
if (delete_screenshot) {
DeleteScreenshot();
}
}
bool TabletModeController::IsInDevTabletMode() const {
return tablet_mode_behavior_ == kOnForDev;
}
void TabletModeController::AddObserver(TabletModeObserver* observer) {
tablet_mode_observers_.AddObserver(observer);
}
void TabletModeController::RemoveObserver(TabletModeObserver* observer) {
tablet_mode_observers_.RemoveObserver(observer);
}
bool TabletModeController::ForceUiTabletModeState(std::optional<bool> enabled) {
if (!enabled.has_value()) {
tablet_mode_behavior_ = kDefault;
} else if (*enabled) {
tablet_mode_behavior_ = kOnForAutotest;
} else {
tablet_mode_behavior_ = kOffForAutotest;
}
// We want to suppress the accelerometer to auto-rotate the screen based on
// the physical orientation, as it will confuse the test scenarios. Note that
// this should not block ScreenOrientationController as the screen may want
// to be rotated for other factors.
AccelerometerReader::GetInstance()->SetEnabled(!enabled.has_value());
return SetIsInTabletPhysicalState(CalculateIsInTabletPhysicalState()) ||
UpdateUiTabletState();
}
void TabletModeController::SetEnabledForTest(bool enabled) {
tablet_mode_behavior_ = enabled ? kOnForTest : kDefault;
SetIsInTabletPhysicalState(enabled);
}
void TabletModeController::OnShellInitialized() {
forced_ui_mode_ = GetUiMode();
switch (forced_ui_mode_) {
case UiMode::kTabletMode:
tablet_mode_behavior_ = kLockInTabletMode;
UpdateUiTabletState();
break;
case UiMode::kClamshell:
tablet_mode_behavior_ = kLockInClamshellMode;
UpdateUiTabletState();
break;
case UiMode::kNone:
break;
}
}
void TabletModeController::OnDidApplyDisplayChanges() {
if (!tablet_mode_behavior_.observe_display_events) {
return;
}
// Display config changes might be due to entering or exiting docked mode, in
// which case the availability of an active internal display changes.
// Therefore we update the physical tablet state of the device.
SetIsInTabletPhysicalState(CalculateIsInTabletPhysicalState());
}
void TabletModeController::OnLoginStatusChanged(LoginStatus login_status) {
if (login_status == LoginStatus::KIOSK_APP &&
ShouldBlockUiTabletModeInKiosk()) {
// Tablet mode is not allowed during the kiosk session. No need to reset the
// `forced_ui_mode_` to the previous state because on kiosk exit Chrome
// restarts, so the `TabletModeController` will be reset.`
// If the device is currently in the UI tablet mode, it will be forced to
// switch to the clamshell UI mode.
forced_ui_mode_ = UiMode::kClamshell;
UpdateUiTabletState();
}
}
void TabletModeController::OnChromeTerminating() {
// The system is about to shut down, so record TabletMode usage interval
// metrics based on whether TabletMode mode is currently active.
RecordTabletModeUsageInterval(CurrentTabletModeIntervalType());
// Only when |tablet_mode_usage_interval_start_time_| is not null,
// |total_tablet_mode_time_| and |total_non_tablet_mode_time_| will have valid
// values.
if (!tablet_mode_usage_interval_start_time_.is_null()) {
DCHECK(CanEnterTabletMode() && initial_input_device_set_up_finished_ &&
have_seen_tablet_mode_event_);
UMA_HISTOGRAM_CUSTOM_COUNTS("Ash.TouchView.TouchViewActiveTotal",
total_tablet_mode_time_.InMinutes(), 1,
base::Days(7).InMinutes(), 50);
UMA_HISTOGRAM_CUSTOM_COUNTS("Ash.TouchView.TouchViewInactiveTotal",
total_non_tablet_mode_time_.InMinutes(), 1,
base::Days(7).InMinutes(), 50);
base::TimeDelta total_runtime =
total_tablet_mode_time_ + total_non_tablet_mode_time_;
if (total_runtime.InSeconds() > 0) {
UMA_HISTOGRAM_PERCENTAGE("Ash.TouchView.TouchViewActivePercentage",
100 * total_tablet_mode_time_.InSeconds() /
total_runtime.InSeconds());
}
}
}
void TabletModeController::OnECLidAngleDriverStatusChanged(bool is_supported) {
is_ec_lid_angle_driver_supported_ = is_supported;
// OnECLidAngleDriverStatusChanged is guaranteed to be called before
// OnAccelerometerUpdated. Thus calling
// StartTrackingTabletUsageMetricsIfApplicable() before or after
// `!is_supported` won't make any difference. The reason is that for
// `!is_supported` case, because we haven't seen any accelerometer data yet,
// we won't start logging here anyway.
// OnECLidAngleDriverStatusChanged can be called before or after
// TabletModeEventReceived. Thus we'll need the logging both here and in
// TabletModeEventReceived function.
StartTrackingTabletUsageMetricsIfApplicable();
if (!is_supported) {
return;
}
// When ChromeOS EC lid angle driver is supported, EC can handle lid angle
// calculation, thus Chrome side lid angle calculation is disabled. In this
// case, TabletModeController no longer listens to accelerometer samples.
// Reset lid angle that might be calculated before lid angle driver is
// read.
lid_angle_ = 0.f;
can_detect_lid_angle_ = false;
if (record_lid_angle_timer_.IsRunning()) {
record_lid_angle_timer_.Stop();
}
AccelerometerReader::GetInstance()->RemoveObserver(this);
}
void TabletModeController::OnAccelerometerUpdated(
const AccelerometerUpdate& update) {
have_seen_accelerometer_data_ = true;
can_detect_lid_angle_ = update.has(ACCELEROMETER_SOURCE_SCREEN) &&
update.has(ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD);
if (!can_detect_lid_angle_) {
if (record_lid_angle_timer_.IsRunning()) {
record_lid_angle_timer_.Stop();
}
} else if (HasActiveInternalDisplay() && tablet_mode_behavior_.use_sensor) {
// Whether or not we enter tablet mode affects whether we handle screen
// rotation, so determine whether to enter tablet mode first.
if (update.IsReadingStable(ACCELEROMETER_SOURCE_SCREEN) &&
update.IsReadingStable(ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD) &&
IsAngleBetweenAccelerometerReadingsStable(update)) {
// update.has(ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD)
// Ignore the reading if it appears unstable. The reading is considered
// unstable if it deviates too much from gravity and/or the magnitude of
// the reading from the lid differs too much from the reading from the
// base.
HandleHingeRotation(update);
}
}
StartTrackingTabletUsageMetricsIfApplicable();
}
void TabletModeController::PowerManagerBecameAvailable(bool available) {
if (!available) {
return;
}
chromeos::PowerManagerClient::Get()->GetSwitchStates(base::BindOnce(
&TabletModeController::OnGetSwitchStates, weak_factory_.GetWeakPtr()));
}
void TabletModeController::LidEventReceived(
chromeos::PowerManagerClient::LidState state,
base::TimeTicks time) {
VLOG(1) << "Lid event received: " << ToString(state);
lid_is_closed_ = state != chromeos::PowerManagerClient::LidState::OPEN;
if (lid_is_closed_) {
// Reset |lid_angle_| to 0.f when lid is closed. The accelerometer readings
// can be wrong when lid is closed, e.g., it can report lid angle to be
// around 360 degrees when lid is nearly closed.
lid_angle_ = 0.f;
}
if (!tablet_mode_behavior_.use_sensor) {
return;
}
SetIsInTabletPhysicalState(CalculateIsInTabletPhysicalState());
}
void TabletModeController::TabletModeEventReceived(
chromeos::PowerManagerClient::TabletMode mode,
base::TimeTicks time) {
have_seen_tablet_mode_event_ = true;
if (tablet_mode_behavior_.use_sensor) {
VLOG(1) << "Tablet mode event received: " << ToString(mode);
const bool on = mode == chromeos::PowerManagerClient::TabletMode::ON;
tablet_mode_switch_is_on_ = on;
tablet_mode_behavior_ = on ? kOnBySensor : kDefault;
SetIsInTabletPhysicalState(CalculateIsInTabletPhysicalState());
}
StartTrackingTabletUsageMetricsIfApplicable();
}
void TabletModeController::SuspendImminent(
power_manager::SuspendImminent::Reason reason) {
// The system is about to suspend, so record TabletMode usage interval metrics
// based on whether TabletMode mode is currently active.
RecordTabletModeUsageInterval(CurrentTabletModeIntervalType());
// Stop listening to any incoming input device changes during suspend as the
// input devices may be removed during suspend and cause the device enter/exit
// tablet mode unexpectedly.
if (IsBoardTypeMarkedAsTabletCapable()) {
ui::DeviceDataManager::GetInstance()->RemoveObserver(this);
bluetooth_devices_observer_.reset();
}
}
void TabletModeController::SuspendDone(base::TimeDelta sleep_duration) {
// We do not want TabletMode usage metrics to include time spent in suspend.
if (!tablet_mode_usage_interval_start_time_.is_null()) {
tablet_mode_usage_interval_start_time_ = base::Time::Now();
}
// Start listening to the input device changes again.
// It might be possible that the suspend request has been cancelled so
// `this` was not removed as an observer of the input device changes. See
// b/271634754 for details.
auto* device_data_manager = ui::DeviceDataManager::GetInstance();
if (IsBoardTypeMarkedAsTabletCapable() &&
!device_data_manager->HasObserver(this)) {
bluetooth_devices_observer_ =
std::make_unique<BluetoothDevicesObserver>(base::BindRepeating(
&TabletModeController::OnBluetoothAdapterOrDeviceChanged,
base::Unretained(this)));
device_data_manager->AddObserver(this);
// Call HandlePointingDeviceAddedOrRemoved() to iterate all available input
// devices just in case we have missed all the notifications from
// DeviceDataManager and BluetoothDevicesObserver when SuspendDone() is
// called.
HandlePointingDeviceAddedOrRemoved();
}
}
void TabletModeController::OnInputDeviceConfigurationChanged(
uint8_t input_device_types) {
if (input_device_types & (ui::InputDeviceEventObserver::kMouse |
ui::InputDeviceEventObserver::kTouchpad |
ui::InputDeviceEventObserver::kPointingStick)) {
if (input_device_types & ui::InputDeviceEventObserver::kMouse) {
VLOG(1) << "Mouse device configuration changed.";
}
if (input_device_types & ui::InputDeviceEventObserver::kTouchpad) {
VLOG(1) << "Touchpad device configuration changed.";
}
if (input_device_types & ui::InputDeviceEventObserver::kPointingStick) {
VLOG(1) << "Pointing stick device configuration changed.";
}
HandlePointingDeviceAddedOrRemoved();
}
}
void TabletModeController::OnDeviceListsComplete() {
initial_input_device_set_up_finished_ = true;
HandlePointingDeviceAddedOrRemoved();
StartTrackingTabletUsageMetricsIfApplicable();
}
void TabletModeController::OnLayerAnimationStarted(
ui::LayerAnimationSequence* sequence) {}
void TabletModeController::OnLayerAnimationAborted(
ui::LayerAnimationSequence* sequence) {
if (!transition_tracker_ || !ShouldObserveSequence(sequence)) {
return;
}
StopObservingAnimation(/*record_stats=*/false, /*delete_screenshot=*/true);
}
void TabletModeController::OnLayerAnimationEnded(
ui::LayerAnimationSequence* sequence) {
// This may be called before |OnLayerAnimationScheduled()| if tablet is
// entered/exited while an animation is in progress, so we won't get
// stats/screenshot in those cases.
// TODO(sammiequon): We may want to remove the |transition_tracker_| check and
// simplify things since those are edge cases.
if (!transition_tracker_ || !ShouldObserveSequence(sequence)) {
return;
}
StopObservingAnimation(/*record_stats=*/true, /*delete_screenshot=*/true);
}
void TabletModeController::OnLayerAnimationScheduled(
ui::LayerAnimationSequence* sequence) {
if (!ShouldObserveSequence(sequence)) {
return;
}
if (!transition_tracker_) {
transition_tracker_ =
animating_layer_->GetCompositor()->RequestNewThroughputTracker();
transition_tracker_->Start(metrics_util::ForSmoothnessV3(
base::BindRepeating(&ReportTrasitionSmoothness,
display::Screen::GetScreen()->GetTabletState() ==
display::TabletState::kEnteringTabletMode)));
return;
}
// If another animation is scheduled while the animation we were originally
// watching is still animating, abort and do not log stats as the stats will
// not be accurate.
StopObservingAnimation(/*record_stats=*/false, /*delete_screenshot=*/true);
}
void TabletModeController::LayerDestroyed(ui::Layer* layer) {
DCHECK_EQ(animating_layer_, layer);
animating_layer_->RemoveObserver(this);
animating_layer_->GetAnimator()->RemoveObserver(this);
animating_layer_ = nullptr;
}
void TabletModeController::SetEnabledForDev(bool enabled) {
tablet_mode_behavior_ = enabled ? kOnForDev : kDefault;
force_notify_events_blocking_changed_ = true;
SetIsInTabletPhysicalState(enabled);
}
bool TabletModeController::ShouldShowOverviewButton() const {
return AreInternalInputDeviceEventsBlocked() ||
tablet_mode_behavior_.always_show_overview_button;
}
bool TabletModeController::CanEnterTabletMode() const {
// If ChromeOS EC lid angle driver is supported, EC can handle lid angle
// calculation, and trigger tablet mode at some point.
// Otherwise, lid angle calculation is done on Chrome side for convertible
// device. If we have ever seen accelerometer data, then HandleHingeRotation
// may trigger tablet mode at some point in the future.
return IsBoardTypeMarkedAsTabletCapable() &&
(is_ec_lid_angle_driver_supported_.value_or(false) ||
have_seen_accelerometer_data_);
}
////////////////////////////////////////////////////////////////////////////////
// TabletModeController, private:
void TabletModeController::SetTabletModeEnabledInternal(bool should_enable) {
DCHECK_NE(display::Screen::GetScreen()->InTabletMode(), should_enable);
// Hide the context menu on entering tablet mode to prevent users from
// accessing forbidden options. Hide the context menu on exiting tablet mode
// to match behaviors.
HideActiveContextMenu();
// Suspend occlusion tracker when entering or exiting tablet mode.
SuspendOcclusionTracker();
DeleteScreenshot();
if (should_enable) {
Shell::Get()->display_manager()->SetTabletState(
display::TabletState::kEnteringTabletMode);
// Take a screenshot if there is a top window that will get animated.
// Floated windows will always get animated, and if the only window is a
// floated window, we don't take a screenshot since the floated window in
// tablet mode does not cover the whole work area.
// TODO(sammiequon): Handle the case where the top window is not on the
// primary display.
aura::Window* top_window = window_util::GetTopNonFloatedWindow();
const bool top_window_on_primary_display =
top_window &&
top_window->GetRootWindow() == Shell::GetPrimaryRootWindow();
// If the top window was already animating (eg. tablet mode event received
// while create window animation still running), skip taking the screenshot.
// It will take a performance hit but will remove cases where the screenshot
// might not get deleted because of the extra animation observer methods
// getting fired.
const bool top_window_animating =
top_window && top_window->layer()->GetAnimator()->is_animating();
// We'll keep overview active after clamshell <-> tablet mode transition if
// it was active before transition, do not take screenshot if overview is
// active in this case.
const bool overview_remain_active =
Shell::Get()->overview_controller()->InOverviewSession();
if (use_screenshot_for_test && top_window_on_primary_display &&
!top_window_animating && !overview_remain_active) {
TakeScreenshot(top_window);
} else {
FinishInitTabletMode();
}
} else {
// We may have entered tablet mode, then tried to exit before the screenshot
// was taken. In this case `tablet_mode_window_manager_` will be null.
if (tablet_mode_window_manager_) {
tablet_mode_window_manager_->SetIgnoreWmEventsForExit();
}
Shell::Get()->display_manager()->SetTabletState(
display::TabletState::kExitingTabletMode);
// Many events can lead to shelf config updates as a result of
// kInClamshellMode event. Update the shelf config during "ending"
// stage rather than the "ended", so `in_tablet_mode_` gets updated
// correctly, and the shelf bounds are stabilized early so as not to have
// multiple unnecessary work-area bounds changes.
ShelfConfig::Get()->UpdateForTabletMode(/*in_tablet_mode=*/false);
if (tablet_mode_window_manager_) {
tablet_mode_window_manager_->Shutdown(
TabletModeWindowManager::ShutdownReason::kExitTabletUIMode);
}
tablet_mode_window_manager_.reset();
base::RecordAction(base::UserMetricsAction("Touchview_Disabled"));
RecordTabletModeUsageInterval(TABLET_MODE_INTERVAL_ACTIVE);
Shell::Get()->display_manager()->SetTabletState(
display::TabletState::kInClamshellMode);
VLOG(1) << "Exit tablet mode.";
UpdateInternalInputDevicesEventBlocker();
Shell::Get()->cursor_manager()->ShowCursor();
}
}
void TabletModeController::HandleHingeRotation(
const AccelerometerUpdate& update) {
static const gfx::Vector3dF hinge_vector(1.0f, 0.0f, 0.0f);
gfx::Vector3dF base_reading =
update.GetVector(ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD);
gfx::Vector3dF lid_reading = update.GetVector(ACCELEROMETER_SOURCE_SCREEN);
// As the hinge approaches a vertical angle, the base and lid accelerometers
// approach the same values making any angle calculations highly inaccurate.
// Smooth out instantaneous acceleration when nearly vertical to increase
// accuracy.
float largest_hinge_acceleration =
std::max(std::abs(base_reading.x()), std::abs(lid_reading.x()));
float smoothing_ratio = std::clamp(
(largest_hinge_acceleration - kHingeVerticalSmoothingStart) /
(kHingeVerticalSmoothingMaximum - kHingeVerticalSmoothingStart),
0.0f, 1.0f);
// We cannot trust the computed lid angle when the device is held vertically.
bool is_angle_reliable =
largest_hinge_acceleration <= kHingeVerticalSmoothingMaximum;
base_smoothed_.Scale(smoothing_ratio);
base_reading.Scale(1.0f - smoothing_ratio);
base_smoothed_.Add(base_reading);
lid_smoothed_.Scale(smoothing_ratio);
lid_reading.Scale(1.0f - smoothing_ratio);
lid_smoothed_.Add(lid_reading);
if (tablet_mode_switch_is_on_) {
return;
}
// Do not calculate lid angle when lid is closed to prevent the device
// accidentally entering tablet mode. The angle calculated when lid is closed
// is not accurate (the angle between the base and the lid might be a minus
// value when lid is closed, and since we do adjustment for minus values, the
// angle might be in the same range as tablet mode angle range).
if (lid_is_closed_) {
return;
}
// Ignore the component of acceleration parallel to the hinge for the purposes
// of hinge angle calculation.
gfx::Vector3dF base_flattened(base_smoothed_);
gfx::Vector3dF lid_flattened(lid_smoothed_);
base_flattened.set_x(0.0f);
lid_flattened.set_x(0.0f);
// Compute the angle between the base and the lid.
lid_angle_ = 180.0f - gfx::ClockwiseAngleBetweenVectorsInDegrees(
base_flattened, lid_flattened, hinge_vector);
if (lid_angle_ < 0.0f) {
lid_angle_ += 360.0f;
}
lid_angle_is_stable_ = is_angle_reliable && lid_angle_ >= kMinStableAngle &&
lid_angle_ <= kMaxStableAngle;
if (lid_angle_is_stable_) {
// Reset the timestamp of first unstable lid angle because we get a stable
// reading.
first_unstable_lid_angle_time_ = base::TimeTicks();
} else if (first_unstable_lid_angle_time_.is_null()) {
first_unstable_lid_angle_time_ = tick_clock_->NowTicks();
}
const bool new_tablet_physical_state = CalculateIsInTabletPhysicalState();
tablet_mode_behavior_ = new_tablet_physical_state ? kOnBySensor : kDefault;
SetIsInTabletPhysicalState(new_tablet_physical_state);
// Start reporting the lid angle if we aren't already doing so.
if (!record_lid_angle_timer_.IsRunning()) {
record_lid_angle_timer_.Start(
FROM_HERE, kRecordLidAngleInterval,
base::BindRepeating(&TabletModeController::RecordLidAngle,
base::Unretained(this)));
}
}
void TabletModeController::OnGetSwitchStates(
std::optional<chromeos::PowerManagerClient::SwitchStates> result) {
if (!result.has_value()) {
return;
}
LidEventReceived(result->lid_state, base::TimeTicks::Now());
TabletModeEventReceived(result->tablet_mode, base::TimeTicks::Now());
}
bool TabletModeController::CanUseUnstableLidAngle() const {
DCHECK(!first_unstable_lid_angle_time_.is_null());
const base::TimeTicks now = tick_clock_->NowTicks();
DCHECK(now >= first_unstable_lid_angle_time_);
const base::TimeDelta elapsed_time = now - first_unstable_lid_angle_time_;
return elapsed_time >= kUnstableLidAngleDuration;
}
void TabletModeController::RecordTabletModeUsageInterval(
TabletModeIntervalType type) {
// If |tablet_mode_usage_interval_start_time_| is null, do not record any
// tablet mode usage metrics. It may happen when we have some false positive
// tablet mode activations during startup.
if (tablet_mode_usage_interval_start_time_.is_null()) {
return;
}
DCHECK(CanEnterTabletMode() && initial_input_device_set_up_finished_ &&
have_seen_tablet_mode_event_);
base::Time current_time = base::Time::Now();
base::TimeDelta delta = current_time - tablet_mode_usage_interval_start_time_;
switch (type) {
case TABLET_MODE_INTERVAL_INACTIVE:
UMA_HISTOGRAM_LONG_TIMES(kTabletInactiveTimeHistogramName, delta);
total_non_tablet_mode_time_ += delta;
break;
case TABLET_MODE_INTERVAL_ACTIVE:
UMA_HISTOGRAM_LONG_TIMES(kTabletActiveTimeHistogramName, delta);
total_tablet_mode_time_ += delta;
break;
}
tablet_mode_usage_interval_start_time_ = current_time;
}
void TabletModeController::RecordLidAngle() {
DCHECK(can_detect_lid_angle_);
base::LinearHistogram::FactoryGet(
kLidAngleHistogramName, /*minimum=*/1, /*maximum=*/360,
/*bucket_count=*/50, base::HistogramBase::kUmaTargetedHistogramFlag)
->Add(std::round(lid_angle_));
}
TabletModeController::TabletModeIntervalType
TabletModeController::CurrentTabletModeIntervalType() {
return display::Screen::GetScreen()->InTabletMode()
? TABLET_MODE_INTERVAL_ACTIVE
: TABLET_MODE_INTERVAL_INACTIVE;
}
void TabletModeController::HandlePointingDeviceAddedOrRemoved() {
if (!initial_input_device_set_up_finished_) {
return;
}
bool has_external_pointing_device = false;
bool has_internal_pointing_device = false;
// Check if there is an external and internal mouse or touchpad device.
CheckHasPointingDevices(
ui::DeviceDataManager::GetInstance()->GetMouseDevices(),
bluetooth_devices_observer_.get(), has_external_pointing_device,
has_internal_pointing_device);
if (!has_external_pointing_device || !has_internal_pointing_device) {
CheckHasPointingDevices(
ui::DeviceDataManager::GetInstance()->GetTouchpadDevices(),
bluetooth_devices_observer_.get(), has_external_pointing_device,
has_internal_pointing_device);
}
if (!has_external_pointing_device || !has_internal_pointing_device) {
CheckHasPointingDevices(
ui::DeviceDataManager::GetInstance()->GetPointingStickDevices(),
bluetooth_devices_observer_.get(), has_external_pointing_device,
has_internal_pointing_device);
}
const bool changed =
(has_external_pointing_device_ != has_external_pointing_device) ||
(has_internal_pointing_device_ != has_internal_pointing_device);
if (!changed) {
return;
}
has_external_pointing_device_ = has_external_pointing_device;
has_internal_pointing_device_ = has_internal_pointing_device;
// We only need to update UI state if observed internal pointing device or
// external pointing device changed.
if (tablet_mode_behavior_.observe_pointer_device_events) {
UpdateUiTabletState();
}
}
void TabletModeController::OnBluetoothAdapterOrDeviceChanged(
device::BluetoothDevice* device) {
// We only care about pointing type bluetooth device change. Note KEYBOARD
// type is also included here as sometimes a bluetooth keyboard comes with a
// touch pad.
if (!device ||
device->GetDeviceType() == device::BluetoothDeviceType::MOUSE ||
device->GetDeviceType() ==
device::BluetoothDeviceType::KEYBOARD_MOUSE_COMBO ||
device->GetDeviceType() == device::BluetoothDeviceType::KEYBOARD ||
device->GetDeviceType() == device::BluetoothDeviceType::TABLET) {
VLOG(1) << "Bluetooth device configuration changed.";
HandlePointingDeviceAddedOrRemoved();
}
}
void TabletModeController::UpdateInternalInputDevicesEventBlocker() {
// Internal input devices should be blocked (as long as the current
// tablet_mode_behavior_ allows it) if we're in UI tablet mode, or if the
// device is in physical tablet state.
// Note that |is_in_tablet_physical_state_| takes into account whether the
// device is in docked mode (with no active internal display), in which case
// internal input devices should NOT be blocked, since the user may still want
// to use the internal keyboard and mouse in docked mode. This can happen if
// the user turns off the internal display without closing the lid by means of
// setting the brightness to 0.
const bool should_block_internal_events =
tablet_mode_behavior_.block_internal_input_device &&
(display::Screen::GetScreen()->InTabletMode() ||
is_in_tablet_physical_state_);
if (should_block_internal_events == AreInternalInputDeviceEventsBlocked()) {
if (force_notify_events_blocking_changed_) {
for (auto& observer : tablet_mode_observers_) {
observer.OnTabletModeEventsBlockingChanged();
}
force_notify_events_blocking_changed_ = false;
}
return;
}
event_blocker_->UpdateInternalInputDevices(should_block_internal_events);
for (auto& observer : tablet_mode_observers_) {
observer.OnTabletModeEventsBlockingChanged();
}
}
void TabletModeController::SuspendOcclusionTracker() {
occlusion_tracker_reset_timer_.Stop();
occlusion_tracker_pauser_ =
std::make_unique<aura::WindowOcclusionTracker::ScopedPause>();
occlusion_tracker_reset_timer_.Start(FROM_HERE, kOcclusionTrackerTimeout,
this,
&TabletModeController::ResetPauser);
}
void TabletModeController::ResetPauser() {
occlusion_tracker_pauser_.reset();
}
void TabletModeController::FinishInitTabletMode() {
DCHECK_EQ(display::TabletState::kEnteringTabletMode,
display::Screen::GetScreen()->GetTabletState());
// Transition shelf to tablet mode state, now that the screenshot for tablet
// mode transition was taken. Taking screenshot recreates shelf container
// layer, and uses the original layer - changing shelf state before the
// screenshot is taken would change the shelf appearance, and could cause
// issues where the original shelf widget layer is not re-painted correctly in
// response to a paint schedule for tablet mode state change.
// Update the shelf state befire initiating tablet mode window state changes
// to avoid negative impact of window work-area changes (due to changes in
// shelf bounds) during window state transition on the animation smoothness
// https://crbug.com/1044316.
ShelfConfig::Get()->UpdateForTabletMode(/*in_tablet_mode=*/true);
tablet_mode_window_manager_ = std::make_unique<TabletModeWindowManager>();
tablet_mode_window_manager_->Init();
base::RecordAction(base::UserMetricsAction("Touchview_Enabled"));
RecordTabletModeUsageInterval(TABLET_MODE_INTERVAL_INACTIVE);
Shell::Get()->display_manager()->SetTabletState(
display::TabletState::kInTabletMode);
// In some cases, TabletModeWindowManager::TabletModeWindowManager uses
// split view to represent windows that were snapped in desktop mode. If
// there is a window snapped on one side but no window snapped on the other
// side, then overview mode should be started (to be seen on the side with
// no snapped window).
const auto state =
SplitViewController::Get(Shell::GetPrimaryRootWindow())->state();
if (state == SplitViewController::State::kPrimarySnapped ||
state == SplitViewController::State::kSecondarySnapped) {
Shell::Get()->overview_controller()->StartOverview(
OverviewStartAction::kSplitView);
}
UpdateInternalInputDevicesEventBlocker();
Shell::Get()->cursor_manager()->HideCursor();
VLOG(1) << "Enter tablet mode.";
}
void TabletModeController::DeleteScreenshot() {
if (screenshot_layer_) {
VLOG(1) << "Tablet screenshot layer destroyed.";
}
screenshot_layer_.reset();
screenshot_taken_callback_.Cancel();
screenshot_set_callback_.Cancel();
ResetDestroyObserver();
container_hider_.reset();
}
void TabletModeController::ResetDestroyObserver() {
destroy_observer_.reset();
}
void TabletModeController::TakeScreenshot(aura::Window* top_window) {
DCHECK(!top_window->IsRootWindow());
destroy_observer_ = std::make_unique<DestroyObserver>(
top_window, base::BindOnce(&TabletModeController::ResetDestroyObserver,
weak_factory_.GetWeakPtr()));
screenshot_set_callback_.Reset(base::BindOnce(
&TabletModeController::FinishInitTabletMode, weak_factory_.GetWeakPtr()));
auto* screenshot_window = top_window->GetRootWindow()->GetChildById(
kShellWindowId_ScreenAnimationContainer);
base::OnceClosure callback = screenshot_set_callback_.callback();
aura::Window* root_window = top_window->GetRootWindow();
container_hider_ = std::make_unique<ScopedContainerHider>(root_window);
// Request a screenshot.
screenshot_taken_callback_.Reset(base::BindOnce(
&TabletModeController::OnLayerCopyed, weak_factory_.GetWeakPtr(),
std::move(callback), root_window));
CopyLayerContentToNewLayer(screenshot_window->layer(),
screenshot_taken_callback_.callback());
VLOG(1) << "Tablet screenshot requested.";
}
void TabletModeController::OnLayerCopyed(
base::OnceClosure on_screenshot_taken,
aura::Window* root_window,
std::unique_ptr<ui::Layer> copy_layer) {
aura::Window* top_window =
destroy_observer_ ? destroy_observer_->window() : nullptr;
ResetDestroyObserver();
container_hider_.reset();
// Cancel if the root window is deleted while taking a screenshot.
if (!base::Contains(Shell::GetAllRootWindows(), root_window)) {
return;
}
if (!copy_layer || !top_window) {
std::move(on_screenshot_taken).Run();
return;
}
// Stack the screenshot under |top_window|, to fully occlude all windows
// except |top_window| for the duration of the enter tablet mode animation.
screenshot_layer_ = std::move(copy_layer);
top_window->parent()->layer()->Add(screenshot_layer_.get());
screenshot_layer_->SetBounds(top_window->GetRootWindow()->bounds());
top_window->parent()->layer()->StackBelow(screenshot_layer_.get(),
top_window->layer());
std::move(on_screenshot_taken).Run();
VLOG(1) << "Tablet screenshot layer created.";
}
bool TabletModeController::CalculateIsInTabletPhysicalState() const {
switch (tablet_mode_behavior_.force_physical_tablet_state) {
case ForcePhysicalTabletState::kDefault:
// Don't return forced result. Check the hardware configuration.
break;
case ForcePhysicalTabletState::kForceTabletMode:
return true;
case ForcePhysicalTabletState::kForceClamshellMode:
return false;
}
if (!HasActiveInternalDisplay()) {
return false;
}
// For updated EC, the tablet mode switch activates at 200 degrees, and
// deactivates at 160 degrees.
// For old EC, the tablet mode switch activates at 300 degrees, so it's
// always reliable when |tablet_mode_switch_is_on_|.
if (tablet_mode_switch_is_on_) {
return true;
}
if (lid_is_closed_) {
return false;
}
if (!can_detect_lid_angle_) {
return false;
}
// Toggle tablet mode on or off when corresponding thresholds are passed.
if (lid_angle_ >= kEnterTabletModeAngle &&
(lid_angle_is_stable_ || CanUseUnstableLidAngle())) {
return true;
}
if (lid_angle_ <= kExitTabletModeAngle && lid_angle_is_stable_) {
// For angles that are in the exit range, we only consider the stable ones,
// (i.e. we don't check `CanUseUnstableLidAngle()`) in order to avoid
// changing the mode when the lid is almost closed, or recently opened.
return false;
}
// The state should remain the same.
return is_in_tablet_physical_state_;
}
bool TabletModeController::ShouldUiBeInTabletMode() const {
if (forced_ui_mode_ == UiMode::kTabletMode) {
return true;
}
if (forced_ui_mode_ == UiMode::kClamshell) {
return false;
}
if (!tablet_mode_behavior_.observe_pointer_device_events) {
return is_in_tablet_physical_state_;
}
// If this is a tablet capable device, and `OnDeviceListsComplete()` has
// not been received yet, then skip further checking and don't enter tablet
// mode, since `has_external_pointing_device_` and
// `has_internal_pointing_device_` are not accurate yet.
if (IsBoardTypeMarkedAsTabletCapable() &&
!initial_input_device_set_up_finished_) {
return false;
}
if (has_external_pointing_device_) {
return false;
}
if (is_in_tablet_physical_state_) {
return true;
}
return !has_internal_pointing_device_ && CanEnterTabletMode() &&
HasActiveInternalDisplay() && base::SysInfo::IsRunningOnChromeOS();
}
bool TabletModeController::SetIsInTabletPhysicalState(bool new_state) {
if (new_state == is_in_tablet_physical_state_) {
return false;
}
is_in_tablet_physical_state_ = new_state;
for (auto& observer : tablet_mode_observers_) {
observer.OnTabletPhysicalStateChanged();
}
// InputDeviceBlocker must always be updated, but don't update it here if the
// UI state has changed because it's already done.
if (UpdateUiTabletState()) {
return true;
}
UpdateInternalInputDevicesEventBlocker();
return false;
}
bool TabletModeController::UpdateUiTabletState() {
const bool should_be_in_tablet_mode = ShouldUiBeInTabletMode();
if (should_be_in_tablet_mode ==
display::Screen::GetScreen()->InTabletMode()) {
return false;
}
SetTabletModeEnabledInternal(should_be_in_tablet_mode);
Shell::Get()
->accessibility_controller()
->TriggerAccessibilityAlertWithMessage(l10n_util::GetStringUTF8(
should_be_in_tablet_mode ? IDS_ASH_SWITCH_TO_TABLET_MODE
: IDS_ASH_SWITCH_TO_LAPTOP_MODE));
return true;
}
void TabletModeController::StartTrackingTabletUsageMetricsIfApplicable() {
if (!CanEnterTabletMode() || !initial_input_device_set_up_finished_ ||
!have_seen_tablet_mode_event_ ||
!tablet_mode_usage_interval_start_time_.is_null()) {
return;
}
tablet_mode_usage_interval_start_time_ = base::Time::Now();
}
} // namespace ash