blob: 19fe57191844c58c2c45046ce8edcfac099db6e0 [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/wm/tablet_mode/tablet_mode_controller.h"
#include <algorithm>
#include <string>
#include <utility>
#include "ash/kiosk_next/kiosk_next_shell_controller_impl.h"
#include "ash/public/cpp/ash_switches.h"
#include "ash/public/cpp/fps_counter.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/tablet_mode.h"
#include "ash/public/cpp/tablet_mode_toggle_observer.h"
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/tablet_mode/internal_input_devices_event_blocker.h"
#include "ash/wm/tablet_mode/tablet_mode_observer.h"
#include "ash/wm/tablet_mode/tablet_mode_window_manager.h"
#include "ash/wm/window_state.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/location.h"
#include "base/metrics/histogram.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/time/default_tick_clock.h"
#include "base/time/tick_clock.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "components/viz/common/frame_sinks/copy_output_request.h"
#include "components/viz/common/frame_sinks/copy_output_result.h"
#include "third_party/khronos/GLES2/gl2.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/compositor/layer_animation_sequence.h"
#include "ui/display/display.h"
#include "ui/display/manager/display_manager.h"
#include "ui/events/devices/input_device.h"
#include "ui/events/devices/input_device_manager.h"
#include "ui/events/event.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/geometry/vector3d_f.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
// The hinge angle at which to enter tablet mode.
const 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.
const 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).
const float kMinStableAngle = 20.0f;
const 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::TimeDelta::FromSeconds(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.
const 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.
const 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.
const float kNoisyMagnitudeDeviation = 1.0f;
// Interval between calls to RecordLidAngle().
constexpr base::TimeDelta kRecordLidAngleInterval =
base::TimeDelta::FromHours(1);
// Time that should wait to reset |occlusion_tracker_pauser_| on
// entering/exiting tablet mode.
constexpr base::TimeDelta kOcclusionTrackerTimeout =
base::TimeDelta::FromMilliseconds(500);
// 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 true for unit tests so tablet mode can be changed synchronously.
bool force_no_screenshot = false;
// 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;
}
bool ShouldInitTabletModeController() {
return base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kAshEnableTabletMode);
}
// Checks the command line to see which force tablet mode is turned on, if
// any.
TabletModeController::UiMode GetTabletMode() {
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() {
return display::Display::HasInternalDisplay() &&
Shell::Get()->display_manager()->IsActiveDisplayId(
display::Display::InternalDisplayId());
}
bool IsTransformAnimationSequence(ui::LayerAnimationSequence* sequence) {
DCHECK(sequence);
return sequence->properties() & ui::LayerAnimationElement::TRANSFORM;
}
std::unique_ptr<ui::Layer> CreateLayerFromScreenshotResult(
std::unique_ptr<viz::CopyOutputResult> copy_result) {
DCHECK(!copy_result->IsEmpty());
DCHECK_EQ(copy_result->format(), viz::CopyOutputResult::Format::RGBA_TEXTURE);
const gfx::Size layer_size = copy_result->size();
viz::TransferableResource transferable_resource =
viz::TransferableResource::MakeGL(
copy_result->GetTextureResult()->mailbox, GL_LINEAR, GL_TEXTURE_2D,
copy_result->GetTextureResult()->sync_token, layer_size,
/*is_overlay_candidate=*/false);
std::unique_ptr<viz::SingleReleaseCallback> release_callback =
copy_result->TakeTextureOwnership();
auto screenshot_layer = std::make_unique<ui::Layer>();
screenshot_layer->SetTransferableResource(
transferable_resource, std::move(release_callback), layer_size);
return screenshot_layer;
}
} // namespace
// Class which records animation smoothness when entering or exiting tablet
// mode. No stats should be recorded if no windows are animated.
class TabletModeController::TabletModeTransitionFpsCounter : public FpsCounter {
public:
TabletModeTransitionFpsCounter(ui::Compositor* compositor,
bool enter_tablet_mode)
: FpsCounter(compositor), enter_tablet_mode_(enter_tablet_mode) {}
~TabletModeTransitionFpsCounter() override = default;
void LogUma() {
int smoothness = ComputeSmoothness();
if (smoothness < 0)
return;
if (enter_tablet_mode_)
UMA_HISTOGRAM_PERCENTAGE(kTabletModeEnterHistogram, smoothness);
else
UMA_HISTOGRAM_PERCENTAGE(kTabletModeExitHistogram, smoothness);
}
bool enter_tablet_mode() const { return enter_tablet_mode_; }
private:
bool enter_tablet_mode_;
DISALLOW_COPY_AND_ASSIGN(TabletModeTransitionFpsCounter);
};
constexpr char TabletModeController::kLidAngleHistogramName[];
TabletModeController::TabletModeController()
: event_blocker_(new InternalInputDevicesEventBlocker),
tablet_mode_usage_interval_start_time_(base::Time::Now()),
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
// IsTabletModeWindowManagerEnabled to check for the existence of the
// controller.
if (ShouldInitTabletModeController()) {
Shell::Get()->window_tree_host_manager()->AddObserver(this);
AccelerometerReader::GetInstance()->AddObserver(this);
ui::InputDeviceManager::GetInstance()->AddObserver(this);
bluetooth_devices_observer_ =
std::make_unique<BluetoothDevicesObserver>(base::BindRepeating(
&TabletModeController::OnBluetoothAdapterOrDeviceChanged,
base::Unretained(this)));
}
Shell::Get()->kiosk_next_shell_controller()->AddObserver(this);
chromeos::PowerManagerClient* power_manager_client =
chromeos::PowerManagerClient::Get();
power_manager_client->AddObserver(this);
power_manager_client->GetSwitchStates(base::BindOnce(
&TabletModeController::OnGetSwitchStates, weak_factory_.GetWeakPtr()));
}
TabletModeController::~TabletModeController() {
if (tablet_mode_window_manager_)
tablet_mode_window_manager_->Shutdown();
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);
Shell::Get()->kiosk_next_shell_controller()->RemoveObserver(this);
if (ShouldInitTabletModeController()) {
Shell::Get()->window_tree_host_manager()->RemoveObserver(this);
AccelerometerReader::GetInstance()->RemoveObserver(this);
ui::InputDeviceManager::GetInstance()->RemoveObserver(this);
}
chromeos::PowerManagerClient::Get()->RemoveObserver(this);
for (auto& observer : tablet_mode_observers_)
observer.OnTabletControllerDestroyed();
}
// static
void TabletModeController::SetForceNoScreenshotForTest() {
force_no_screenshot = true;
}
// TODO(jcliang): Hide or remove EnableTabletModeWindowManager
// (http://crbug.com/620241).
void TabletModeController::EnableTabletModeWindowManager(bool should_enable) {
bool is_enabled = IsTabletModeWindowManagerEnabled();
if (should_enable == is_enabled)
return;
// 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.
for (auto* root_window : Shell::Get()->GetAllRootWindows())
RootWindowController::ForWindow(root_window)->HideContextMenu();
// Suspend occlusion tracker when entering or exiting tablet mode.
SuspendOcclusionTracker();
DeleteScreenshot();
if (should_enable) {
state_ = State::kEnteringTabletMode;
// Take a screenshot if there is a top window that will get animated.
// TODO(sammiequon): Handle the case where the top window is not on the
// primary display.
aura::Window* top_window = TabletModeWindowManager::GetTopWindow();
bool top_window_on_primary_display =
top_window &&
top_window->GetRootWindow() == Shell::GetPrimaryRootWindow();
if (!force_no_screenshot && top_window_on_primary_display) {
screenshot_set_callback_.Reset(
base::BindOnce(&TabletModeController::FinishInitTabletMode,
weak_factory_.GetWeakPtr()));
TakeScreenshot(top_window, screenshot_set_callback_.callback());
} else {
FinishInitTabletMode();
}
} else {
state_ = State::kExitingTabletMode;
tablet_mode_window_manager_->SetIgnoreWmEventsForExit();
for (auto& observer : tablet_mode_observers_)
observer.OnTabletModeEnding();
tablet_mode_window_manager_->Shutdown();
tablet_mode_window_manager_.reset();
base::RecordAction(base::UserMetricsAction("Touchview_Disabled"));
RecordTabletModeUsageInterval(TABLET_MODE_INTERVAL_ACTIVE);
for (auto& observer : tablet_mode_observers_)
observer.OnTabletModeEnded();
state_ = State::kInClamshellMode;
if (toggle_observer_) // Null at startup and in tests.
toggle_observer_->OnTabletModeToggled(false);
VLOG(1) << "Exit tablet mode.";
}
UpdateInternalInputDevicesEventBlocker();
}
bool TabletModeController::IsTabletModeWindowManagerEnabled() const {
return !!tablet_mode_window_manager_;
}
void TabletModeController::AddWindow(aura::Window* window) {
if (IsTabletModeWindowManagerEnabled())
tablet_mode_window_manager_->AddWindow(window);
}
void TabletModeController::AddObserver(TabletModeObserver* observer) {
tablet_mode_observers_.AddObserver(observer);
}
void TabletModeController::RemoveObserver(TabletModeObserver* observer) {
tablet_mode_observers_.RemoveObserver(observer);
}
bool TabletModeController::ShouldAutoHideTitlebars(views::Widget* widget) {
DCHECK(widget);
const bool tablet_mode = IsTabletModeWindowManagerEnabled();
if (!tablet_mode)
return false;
return widget->IsMaximized() ||
wm::GetWindowState(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 (state_ != State::kEnteringTabletMode &&
state_ != State::kExitingTabletMode) {
return;
}
observed_window_ = window;
observed_layer_ = window->layer();
window->AddObserver(this);
observed_layer_->GetAnimator()->AddObserver(this);
}
void TabletModeController::StopObservingAnimation(bool record_stats,
bool delete_screenshot) {
StopObserving();
if (observed_layer_)
observed_layer_->GetAnimator()->RemoveObserver(this);
observed_layer_ = nullptr;
if (observed_window_)
observed_window_->RemoveObserver(this);
observed_window_ = nullptr;
if (record_stats && fps_counter_)
fps_counter_->LogUma();
fps_counter_.reset();
if (delete_screenshot)
DeleteScreenshot();
}
void TabletModeController::SetTabletModeToggleObserver(
TabletModeToggleObserver* observer) {
DCHECK(observer);
DCHECK(!toggle_observer_);
toggle_observer_ = observer;
}
bool TabletModeController::IsEnabled() const {
return IsTabletModeWindowManagerEnabled();
}
void TabletModeController::SetEnabledForTest(bool enabled) {
// Disable Accelerometer and PowerManagerClient observers to prevent possible
// tablet mode overrides. It won't be possible to physically switch to/from
// tablet mode after calling this function. This is needed for tests that
// run on DUTs and require switching to/back tablet mode in runtime, like some
// ARC++ Tast tests.
AccelerometerReader::GetInstance()->RemoveObserver(this);
chromeos::PowerManagerClient::Get()->RemoveObserver(this);
EnableTabletModeWindowManager(enabled);
}
void TabletModeController::OnShellInitialized() {
force_ui_mode_ = GetTabletMode();
if (force_ui_mode_ == UiMode::kTabletMode)
AttemptEnterTabletMode();
}
void TabletModeController::OnDisplayConfigurationChanged() {
if (!AllowUiModeChange())
return;
if (!HasActiveInternalDisplay()) {
AttemptLeaveTabletMode();
} else if (tablet_mode_switch_is_on_ && !IsTabletModeWindowManagerEnabled()) {
// The internal display has returned, as we are exiting docked mode.
// The device is still in tablet mode, so trigger tablet mode, as this
// switch leads to the ignoring of accelerometer events. When the switch is
// not set the next stable accelerometer readings will trigger maximize
// mode.
AttemptEnterTabletMode();
}
}
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());
if (CanEnterTabletMode()) {
UMA_HISTOGRAM_CUSTOM_COUNTS("Ash.TouchView.TouchViewActiveTotal",
total_tablet_mode_time_.InMinutes(), 1,
base::TimeDelta::FromDays(7).InMinutes(), 50);
UMA_HISTOGRAM_CUSTOM_COUNTS("Ash.TouchView.TouchViewInactiveTotal",
total_non_tablet_mode_time_.InMinutes(), 1,
base::TimeDelta::FromDays(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::OnAccelerometerUpdated(
scoped_refptr<const AccelerometerUpdate> update) {
if (!AllowUiModeChange())
return;
// When ChromeOS EC lid angle driver is present, EC can handle lid angle
// calculation, thus Chrome side lid angle calculation is disabled. In this
// case, TabletModeController no longer listens to accelerometer events.
if (update->HasLidAngleDriver(ACCELEROMETER_SOURCE_SCREEN) ||
update->HasLidAngleDriver(ACCELEROMETER_SOURCE_ATTACHED_KEYBOARD)) {
AccelerometerReader::GetInstance()->RemoveObserver(this);
return;
}
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();
return;
}
if (!HasActiveInternalDisplay())
return;
// 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);
}
}
void TabletModeController::LidEventReceived(
chromeos::PowerManagerClient::LidState state,
const base::TimeTicks& time) {
if (!AllowUiModeChange())
return;
VLOG(1) << "Lid event received: " << static_cast<int>(state);
const bool open = state == chromeos::PowerManagerClient::LidState::OPEN;
lid_is_closed_ = !open;
if (!tablet_mode_switch_is_on_)
AttemptLeaveTabletMode();
}
void TabletModeController::TabletModeEventReceived(
chromeos::PowerManagerClient::TabletMode mode,
const base::TimeTicks& time) {
if (!AllowUiModeChange())
return;
VLOG(1) << "Tablet mode event received: " << static_cast<int>(mode);
const bool on = mode == chromeos::PowerManagerClient::TabletMode::ON;
tablet_mode_switch_is_on_ = on;
// Do not change if docked.
if (!HasActiveInternalDisplay())
return;
// 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 |on|. However we wish to exit tablet mode at a
// smaller angle, so when |on| is false we ignore if it is possible to
// calculate the lid angle.
if (on && !IsTabletModeWindowManagerEnabled()) {
AttemptEnterTabletMode();
} else if (!on && IsTabletModeWindowManagerEnabled() &&
!can_detect_lid_angle_) {
AttemptLeaveTabletMode();
}
// Even if we do not change its ui mode, we should update its input device
// blocker as tablet mode events may come in because of the lid angle/or folio
// keyboard state changes but ui mode might still stay the same.
UpdateInternalInputDevicesEventBlocker();
}
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 (ShouldInitTabletModeController()) {
ui::InputDeviceManager::GetInstance()->RemoveObserver(this);
bluetooth_devices_observer_.reset();
}
}
void TabletModeController::SuspendDone(const base::TimeDelta& sleep_duration) {
// We do not want TabletMode usage metrics to include time spent in suspend.
tablet_mode_usage_interval_start_time_ = base::Time::Now();
// Start listening to the input device changes again.
if (ShouldInitTabletModeController()) {
bluetooth_devices_observer_ =
std::make_unique<BluetoothDevicesObserver>(base::BindRepeating(
&TabletModeController::OnBluetoothAdapterOrDeviceChanged,
base::Unretained(this)));
ui::InputDeviceManager::GetInstance()->AddObserver(this);
// Call HandlePointingDeviceAddedOrRemoved() to iterate all available input
// devices just in case we have missed all the notifications from
// InputDeviceManager 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)) {
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.";
HandlePointingDeviceAddedOrRemoved();
}
}
void TabletModeController::OnDeviceListsComplete() {
HandlePointingDeviceAddedOrRemoved();
}
void TabletModeController::OnKioskNextEnabled() {
kiosk_next_enabled_ = true;
AttemptEnterTabletMode();
}
void TabletModeController::OnLayerAnimationStarted(
ui::LayerAnimationSequence* sequence) {}
void TabletModeController::OnLayerAnimationAborted(
ui::LayerAnimationSequence* sequence) {
if (!fps_counter_ || !IsTransformAnimationSequence(sequence))
return;
StopObservingAnimation(/*record_stats=*/false, /*delete_screenshot=*/true);
}
void TabletModeController::OnLayerAnimationEnded(
ui::LayerAnimationSequence* sequence) {
if (!fps_counter_ || !IsTransformAnimationSequence(sequence))
return;
StopObservingAnimation(/*record_stats=*/true, /*delete_screenshot=*/true);
}
void TabletModeController::OnLayerAnimationScheduled(
ui::LayerAnimationSequence* sequence) {
if (!IsTransformAnimationSequence(sequence))
return;
if (!fps_counter_) {
fps_counter_ = std::make_unique<TabletModeTransitionFpsCounter>(
observed_layer_->GetCompositor(), state_ == State::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::OnWindowDestroying(aura::Window* window) {
DCHECK_EQ(observed_window_, window);
StopObservingAnimation(/*record_stats=*/false, /*delete_screenshot=*/true);
}
void TabletModeController::HandleHingeRotation(
scoped_refptr<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::max(0.0f, std::min(1.0f, (largest_hinge_acceleration -
kHingeVerticalSmoothingStart) /
(kHingeVerticalSmoothingMaximum -
kHingeVerticalSmoothingStart)));
// 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;
// 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;
bool is_angle_stable = is_angle_reliable && lid_angle_ >= kMinStableAngle &&
lid_angle_ <= kMaxStableAngle;
if (is_angle_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();
}
// Toggle tablet mode on or off when corresponding thresholds are passed.
if (is_angle_stable && lid_angle_ <= kExitTabletModeAngle) {
AttemptLeaveTabletMode();
} else if (!lid_is_closed_ && lid_angle_ >= kEnterTabletModeAngle &&
(is_angle_stable || CanUseUnstableLidAngle())) {
AttemptEnterTabletMode();
}
// 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(
base::Optional<chromeos::PowerManagerClient::SwitchStates> result) {
if (!result.has_value())
return;
if (AccelerometerReader::GetInstance()->is_disabled())
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;
}
bool TabletModeController::CanEnterTabletMode() {
// If we have ever seen accelerometer data, then HandleHingeRotation may
// trigger tablet mode at some point in the future.
// All TabletMode-enabled devices can enter tablet mode.
return have_seen_accelerometer_data_ || IsEnabled();
}
void TabletModeController::AttemptEnterTabletMode() {
if (IsTabletModeWindowManagerEnabled() || has_external_pointing_device_) {
UpdateInternalInputDevicesEventBlocker();
return;
}
EnableTabletModeWindowManager(true);
}
void TabletModeController::AttemptLeaveTabletMode() {
if (!IsTabletModeWindowManagerEnabled()) {
UpdateInternalInputDevicesEventBlocker();
return;
}
EnableTabletModeWindowManager(false);
}
void TabletModeController::RecordTabletModeUsageInterval(
TabletModeIntervalType type) {
if (!CanEnterTabletMode())
return;
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("Ash.TouchView.TouchViewInactive", delta);
total_non_tablet_mode_time_ += delta;
break;
case TABLET_MODE_INTERVAL_ACTIVE:
UMA_HISTOGRAM_LONG_TIMES("Ash.TouchView.TouchViewActive", 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, 1 /* minimum */, 360 /* maximum */,
50 /* bucket_count */, base::HistogramBase::kUmaTargetedHistogramFlag)
->Add(std::round(lid_angle_));
}
TabletModeController::TabletModeIntervalType
TabletModeController::CurrentTabletModeIntervalType() {
if (IsTabletModeWindowManagerEnabled())
return TABLET_MODE_INTERVAL_ACTIVE;
return TABLET_MODE_INTERVAL_INACTIVE;
}
bool TabletModeController::AllowUiModeChange() const {
return force_ui_mode_ == UiMode::kNone && !kiosk_next_enabled_;
}
void TabletModeController::HandlePointingDeviceAddedOrRemoved() {
if (!AllowUiModeChange())
return;
bool has_external_pointing_device = false;
// Check if there is an external mouse device.
for (const ui::InputDevice& mouse :
ui::InputDeviceManager::GetInstance()->GetMouseDevices()) {
if (mouse.type == ui::INPUT_DEVICE_USB ||
(mouse.type == ui::INPUT_DEVICE_BLUETOOTH &&
bluetooth_devices_observer_->IsConnectedBluetoothDevice(mouse))) {
has_external_pointing_device = true;
break;
}
}
// Check if there is an external touchpad device.
if (!has_external_pointing_device) {
for (const ui::InputDevice& touch_pad :
ui::InputDeviceManager::GetInstance()->GetTouchpadDevices()) {
if (touch_pad.type == ui::INPUT_DEVICE_USB ||
(touch_pad.type == ui::INPUT_DEVICE_BLUETOOTH &&
bluetooth_devices_observer_->IsConnectedBluetoothDevice(
touch_pad))) {
has_external_pointing_device = true;
break;
}
}
}
if (has_external_pointing_device_ == has_external_pointing_device)
return;
has_external_pointing_device_ = has_external_pointing_device;
// Enter clamshell mode whenever an external pointing device is attached.
if (has_external_pointing_device) {
AttemptLeaveTabletMode();
} else if (HasActiveInternalDisplay() &&
(LidAngleIsInTabletModeRange() || tablet_mode_switch_is_on_)) {
// If there is no external pointing device, only enter tablet mode if docked
// mode is inactive and 1) the lid angle can be detected and is in tablet
// mode angle range. or 2) if the lid angle can't be detected (e.g., tablet
// device or clamshell device) and |tablet_mode_switch_is_on_| is true (it
// can only happen for tablet device as |tablet_mode_switch_is_on_| should
// never be true for a clamshell device).
AttemptEnterTabletMode();
}
}
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() {
bool should_block_internal_events = false;
if (IsTabletModeWindowManagerEnabled()) {
// If we are currently in tablet mode, the internal input events should
// always be blocked.
should_block_internal_events = (force_ui_mode_ == UiMode::kNone);
} else if (HasActiveInternalDisplay() &&
(LidAngleIsInTabletModeRange() || tablet_mode_switch_is_on_)) {
// If we are currently in clamshell mode, the intenral input events should
// only be blocked if the current lid angle belongs to tablet mode angle
// or |tablet_mode_switch_is_on_| is true.
// Note if we don't have an active internal display, the device is currently
// in docked mode, and the user may still want to use the internal keyboard
// and mouse in docked mode, we don't block internal events in this case.
should_block_internal_events = true;
}
if (should_block_internal_events == AreInternalInputDeviceEventsBlocked())
return;
event_blocker_->UpdateInternalInputDevices(should_block_internal_events);
for (auto& observer : tablet_mode_observers_)
observer.OnTabletModeEventsBlockingChanged();
}
bool TabletModeController::LidAngleIsInTabletModeRange() {
return can_detect_lid_angle_ && !lid_is_closed_ &&
lid_angle_ >= kEnterTabletModeAngle;
}
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() {
tablet_mode_window_manager_.reset(new TabletModeWindowManager());
tablet_mode_window_manager_->Init();
base::RecordAction(base::UserMetricsAction("Touchview_Enabled"));
RecordTabletModeUsageInterval(TABLET_MODE_INTERVAL_INACTIVE);
for (auto& observer : tablet_mode_observers_)
observer.OnTabletModeStarted();
// 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 = Shell::Get()->split_view_controller()->state();
if (state == SplitViewState::kLeftSnapped ||
state == SplitViewState::kRightSnapped) {
Shell::Get()->overview_controller()->ToggleOverview();
}
state_ = State::kInTabletMode;
if (toggle_observer_) // Null at startup and in tests.
toggle_observer_->OnTabletModeToggled(true);
VLOG(1) << "Enter tablet mode.";
}
void TabletModeController::DeleteScreenshot() {
screenshot_layer_.reset();
screenshot_taken_callback_.Cancel();
screenshot_set_callback_.Cancel();
}
void TabletModeController::TakeScreenshot(
aura::Window* top_window,
base::OnceClosure on_screenshot_taken) {
DCHECK(top_window);
DCHECK(!top_window->IsRootWindow());
auto* screenshot_window = top_window->GetRootWindow()->GetChildById(
kShellWindowId_ScreenRotationContainer);
// Pause the compositor and hide the top window before taking a screenshot.
// Use opacity zero instead of show/hide to preserve MRU ordering.
const auto roots = Shell::GetAllRootWindows();
for (auto* root : roots)
root->GetHost()->compositor()->SetAllowLocksToExtendTimeout(true);
top_window->layer()->SetOpacity(0.f);
// Request a screenshot.
screenshot_taken_callback_.Reset(base::BindOnce(
&TabletModeController::OnScreenshotTaken, weak_factory_.GetWeakPtr(),
top_window, std::move(on_screenshot_taken)));
const gfx::Rect request_bounds(screenshot_window->layer()->size());
auto screenshot_request = std::make_unique<viz::CopyOutputRequest>(
viz::CopyOutputRequest::ResultFormat::RGBA_TEXTURE,
screenshot_taken_callback_.callback());
screenshot_request->set_area(request_bounds);
screenshot_window->layer()->RequestCopyOfOutput(
std::move(screenshot_request));
top_window->layer()->SetOpacity(1.f);
for (auto* root : roots)
root->GetHost()->compositor()->SetAllowLocksToExtendTimeout(false);
}
void TabletModeController::OnScreenshotTaken(
aura::Window* top_window,
base::OnceClosure on_screenshot_taken,
std::unique_ptr<viz::CopyOutputResult> copy_result) {
if (!copy_result || copy_result->IsEmpty()) {
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_ = CreateLayerFromScreenshotResult(std::move(copy_result));
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();
}
} // namespace ash