1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
ash / wm / tablet_mode / tablet_mode_controller.h [blame]
// 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.
#ifndef ASH_WM_TABLET_MODE_TABLET_MODE_CONTROLLER_H_
#define ASH_WM_TABLET_MODE_TABLET_MODE_CONTROLLER_H_
#include <memory>
#include <optional>
#include "ash/accelerometer/accelerometer_reader.h"
#include "ash/accelerometer/accelerometer_types.h"
#include "ash/ash_export.h"
#include "ash/bluetooth_devices_observer.h"
#include "ash/display/window_tree_host_manager.h"
#include "ash/public/cpp/session/session_observer.h"
#include "ash/public/cpp/tablet_mode.h"
#include "ash/shell_observer.h"
#include "base/feature_list.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "ui/aura/window_occlusion_tracker.h"
#include "ui/compositor/compositor_metrics_tracker.h"
#include "ui/compositor/layer_animation_element.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/layer_observer.h"
#include "ui/compositor/layer_tree_owner.h"
#include "ui/display/manager/display_manager_observer.h"
#include "ui/display/screen.h"
#include "ui/events/devices/input_device_event_observer.h"
#include "ui/gfx/geometry/vector3d_f.h"
namespace aura {
class Window;
}
namespace base {
class TickClock;
}
namespace gfx {
class Vector3dF;
}
namespace ui {
class LayerAnimationSequence;
}
namespace views {
class Widget;
}
namespace ash {
class InternalInputDevicesEventBlocker;
class TabletModeObserver;
class TabletModeWindowManager;
// TODO(b/357489575): cleanup this kill-switch.
BASE_DECLARE_FEATURE(kBlockUiTabletModeInKiosk);
// When EC (Embedded Controller) cannot handle lid angle calculation,
// TabletModeController listens to accelerometer events and automatically
// enters and exits tablet mode when the lid is opened beyond the triggering
// angle and rotates the display to match the device when in tablet mode.
class ASH_EXPORT TabletModeController
: public AccelerometerReader::Observer,
public chromeos::PowerManagerClient::Observer,
public TabletMode,
public ShellObserver,
public display::DisplayManagerObserver,
public SessionObserver,
public ui::InputDeviceEventObserver,
public ui::LayerAnimationObserver,
public ui::LayerObserver {
public:
// Used for keeping track if the user wants the machine to behave as a
// clamshell/tablet regardless of hardware orientation.
// TODO(oshima): Move this to common place.
enum class UiMode {
kNone = 0,
kClamshell,
kTabletMode,
};
// Public so it can be used by unit tests.
constexpr static char kLidAngleHistogramName[] = "Ash.TouchView.LidAngle";
constexpr static char kTabletInactiveTimeHistogramName[] =
"Ash.TouchView.TouchViewInactive";
constexpr static char kTabletActiveTimeHistogramName[] =
"Ash.TouchView.TouchViewActive";
// Enable or disable using a screenshot for testing as it makes the
// initialization flow async, which makes most tests harder to write.
static void SetUseScreenshotForTest(bool use_screenshot);
// Returns the the animation property that we observe when transitioning from
// clamshell to tablet mode.
static ui::LayerAnimationElement::AnimatableProperty
GetObservedTabletTransitionProperty();
TabletModeController();
TabletModeController(const TabletModeController&) = delete;
TabletModeController& operator=(const TabletModeController&) = delete;
~TabletModeController() override;
void Shutdown();
// Add a special window to the TabletModeWindowManager for tracking. This is
// only required for special windows which are handled by other window
// managers like the |MultiUserWindowManagerImpl|.
// If the tablet mode is not enabled no action will be performed.
void AddWindow(aura::Window* window);
// Checks if we should auto hide title bars for the |widget| in tablet mode.
bool ShouldAutoHideTitlebars(views::Widget* widget);
// If |record_lid_angle_timer_| is running, invokes its task and returns true.
// Otherwise, returns false.
[[nodiscard]] bool TriggerRecordLidAngleTimerForTesting();
// Starts observing |window| for animation changes.
void MaybeObserveBoundsAnimation(aura::Window* window);
// Stops observing the window which is being animated from tablet <->
// clamshell.
void StopObservingAnimation(bool record_stats, bool delete_screenshot);
// Returns true if we're in tablet mode for development purpose (please refer
// to kOnForDev for more details.)
bool IsInDevTabletMode() const;
// TabletMode:
void AddObserver(TabletModeObserver* observer) override;
void RemoveObserver(TabletModeObserver* observer) override;
bool AreInternalInputDeviceEventsBlocked() const override;
bool ForceUiTabletModeState(std::optional<bool> enabled) override;
// Do NOT call this directly from unit tests. Instead, please use
// ash::TabletModeControllerTestApi().{Enter/Leave}TabletMode().
// TODO(crbug.com/40942452): Move this to private.
void SetEnabledForTest(bool enabled) override;
// ShellObserver:
void OnShellInitialized() override;
// display::DisplayManagerObserver:
void OnDidApplyDisplayChanges() override;
// SessionObserver:
void OnLoginStatusChanged(LoginStatus login_status) override;
void OnChromeTerminating() override;
// AccelerometerReader::Observer:
void OnECLidAngleDriverStatusChanged(bool is_supported) override;
void OnAccelerometerUpdated(const AccelerometerUpdate& update) override;
// chromeos::PowerManagerClient::Observer:
void PowerManagerBecameAvailable(bool available) override;
void LidEventReceived(chromeos::PowerManagerClient::LidState state,
base::TimeTicks time) override;
void TabletModeEventReceived(chromeos::PowerManagerClient::TabletMode mode,
base::TimeTicks time) override;
void SuspendImminent(power_manager::SuspendImminent::Reason reason) override;
void SuspendDone(base::TimeDelta sleep_duration) override;
// ui::InputDeviceEventObserver:
void OnInputDeviceConfigurationChanged(uint8_t input_device_types) override;
void OnDeviceListsComplete() override;
// ui::LayerAnimationObserver:
void OnLayerAnimationStarted(ui::LayerAnimationSequence* sequence) override;
void OnLayerAnimationEnded(ui::LayerAnimationSequence* sequence) override;
void OnLayerAnimationAborted(ui::LayerAnimationSequence* sequence) override;
void OnLayerAnimationScheduled(ui::LayerAnimationSequence* sequence) override;
// ui::LayerObserver:
void LayerDestroyed(ui::Layer* layer) override;
void increment_app_window_drag_count() { ++app_window_drag_count_; }
void increment_app_window_drag_in_splitview_count() {
++app_window_drag_in_splitview_count_;
}
void increment_tab_drag_count() { ++tab_drag_count_; }
void increment_tab_drag_in_splitview_count() {
++tab_drag_in_splitview_count_;
}
TabletModeWindowManager* tablet_mode_window_manager() {
return tablet_mode_window_manager_.get();
}
bool is_in_tablet_physical_state() const {
return is_in_tablet_physical_state_;
}
float lid_angle() const { return lid_angle_; }
// Enable/disable the tablet mode for development. Please see cc file
// for more details.
void SetEnabledForDev(bool enabled);
// Returns true if the system tray should have a overview button.
bool ShouldShowOverviewButton() const;
// True if it is possible to enter tablet mode in the current
// configuration. If this returns false, it should never be the case that
// tablet mode becomes enabled.
bool CanEnterTabletMode() const;
// ForcePhysicalTabletState is to control physical tablet state. The default
// state is not to force the state, so the tablet-mode controller will observe
// device configurations.
enum class ForcePhysicalTabletState {
kDefault,
kForceTabletMode,
kForceClamshellMode,
};
// Defines how the tablet mode controller controls the
// tablet mode and its transition between clamshell mode.
// This is defined as a public to define constexpr in cc.
struct TabletModeBehavior {
bool use_sensor = true;
bool observe_display_events = true;
bool observe_pointer_device_events = true;
bool block_internal_input_device = false;
bool always_show_overview_button = false;
ForcePhysicalTabletState force_physical_tablet_state =
ForcePhysicalTabletState::kDefault;
bool operator==(const TabletModeBehavior& other) const {
return use_sensor == other.use_sensor &&
observe_display_events == other.observe_display_events &&
observe_pointer_device_events ==
other.observe_pointer_device_events &&
block_internal_input_device == other.block_internal_input_device &&
always_show_overview_button == other.always_show_overview_button &&
force_physical_tablet_state == other.force_physical_tablet_state;
}
};
private:
class DestroyObserver;
class ScopedContainerHider;
friend class TabletModeControllerTestApi;
// Used for recording metrics for intervals of time spent in
// and out of TabletMode.
enum TabletModeIntervalType {
TABLET_MODE_INTERVAL_INACTIVE,
TABLET_MODE_INTERVAL_ACTIVE
};
// Turn the always tablet mode window manager on or off.
void SetTabletModeEnabledInternal(bool should_enable);
// If EC cannot handle lid angle calc, browser detects hinge rotation from
// base and lid accelerometers and automatically start / stop tablet mode.
void HandleHingeRotation(const AccelerometerUpdate& update);
void OnGetSwitchStates(
std::optional<chromeos::PowerManagerClient::SwitchStates> result);
// Returns true if unstable lid angle can be used. The lid angle that falls in
// the unstable zone ([0, 20) and (340, 360] degrees) is considered unstable
// due to the potential erroneous accelerometer readings. Immediately using
// the unstable angle to trigger tablet mode is error-prone. So we wait for
// a certain range of time before using unstable angle.
bool CanUseUnstableLidAngle() const;
// Record UMA stats tracking TabletMode usage. If |type| is
// TABLET_MODE_INTERVAL_INACTIVE, then record that TabletMode has been
// inactive from |tablet_mode_usage_interval_start_time_| until now.
// Similarly, record that TabletMode has been active if |type| is
// TABLET_MODE_INTERVAL_ACTIVE.
void RecordTabletModeUsageInterval(TabletModeIntervalType type);
// Reports an UMA histogram containing the value of |lid_angle_|.
// Called periodically by |record_lid_angle_timer_|. If EC can handle lid
// angle calc, |lid_angle_| is unavailable to browser.
void RecordLidAngle();
// Returns TABLET_MODE_INTERVAL_ACTIVE if TabletMode is currently active,
// otherwise returns TABLET_MODE_INTERNAL_INACTIVE.
TabletModeIntervalType CurrentTabletModeIntervalType();
// Called when a pointing device config is changed, or when a device list is
// sent from device manager. This will exit tablet mode if needed.
void HandlePointingDeviceAddedOrRemoved();
// Callback function of |bluetooth_devices_observer_|. Called when the
// bluetooth adapter or |device| changes.
void OnBluetoothAdapterOrDeviceChanged(device::BluetoothDevice* device);
// Update the internal mouse and keyboard event blocker |event_blocker_|
// according to current configuration. The internal input events should be
// blocked if 1) we are currently in tablet mode or 2) we are currently in
// laptop mode but the lid is flipped over (i.e., we are in laptop mode
// because of an external attached mouse).
void UpdateInternalInputDevicesEventBlocker();
// Suspends |occlusion_tracker_pauser_| for the duration of
// kOcclusionTrackTimeout.
void SuspendOcclusionTracker();
// Resets |occlusion_tracker_pauser_|.
void ResetPauser();
// Deletes the enter tablet mode screenshot and associated callbacks.
void DeleteScreenshot();
void ResetDestroyObserver();
// Finishes initializing for tablet mode. May be called async if a screenshot
// was requested while starting initializing.
void FinishInitTabletMode();
// Takes a screenshot of everything in the rotation container, except for
// |top_window|.
void TakeScreenshot(aura::Window* top_window);
// Called when a screenshot is taken. Creates |screenshot_widget_| which holds
// the screenshot results and stacks it under top window. |root_window|
// specifies on which root window the screen shot is taken.
void OnLayerCopyed(base::OnceClosure on_screenshot_taken,
aura::Window* root_window,
std::unique_ptr<ui::Layer> copy_layer);
// Calculates whether the device is currently in a physical tablet state,
// using the most recent seen device events such as lid angle changes.
bool CalculateIsInTabletPhysicalState() const;
// Returns whether the UI should be in tablet mode based on the current
// physical tablet state, the availability of external input devices, and
// whether the UI is forced in a particular mode via command-line flags.
bool ShouldUiBeInTabletMode() const;
// Sets |is_in_tablet_physical_state_| to |new_state| and potentially updating
// the UI tablet mode state if needed. Returns true if the
// |is_in_tablet_physical_state_| has been changed.
bool SetIsInTabletPhysicalState(bool new_state);
// Updates the UI by either entering or exiting UI tablet mode if necessary
// based on the current state. Returns true if there's a change in the UI
// tablet mode state, false otherwise.
bool UpdateUiTabletState();
// Starts tracking the tablet usage metrics if the following conditions are
// all meet:
// 1. The device is capable of entering tablet mode.
// 2. The device has seen accelerometer data or the device has EC lid angle
// driver supported.
// 3. The device has seen tablet mode event and has responded to tablet mode
// event.
// 4. Initial input device setup has been finished. At this moment, we know
// the device has responded to the input device change.
// 5. We haven't started tracking the tablet usage metrics.
// The conditions 1, 2, 3, 4 are to avoid the false recordings that can happen
// at startup. During startup, since all these above events are async, plus
// potential sensor noises, the device can change its ui mode a couple times
// before it stabilized to its correct ui mode, thus we don't want to log the
// tablet usage metrics before the device has received all necessary events
// and has stabilized its ui mode.
void StartTrackingTabletUsageMetricsIfApplicable();
// The tablet window manager (if enabled).
std::unique_ptr<TabletModeWindowManager> tablet_mode_window_manager_;
// A helper class which when instantiated will block native events from the
// internal keyboard and touchpad.
std::unique_ptr<InternalInputDevicesEventBlocker> event_blocker_;
// Whether we have ever seen accelerometer data. When ChromeOS EC lid angle
// driver is supported, convertible device cannot see accelerometer data.
bool have_seen_accelerometer_data_ = false;
// Whether we have ever seen tablet mode event sent from power manager.
bool have_seen_tablet_mode_event_ = false;
// If ECLidAngleDriverStatus is supported, Chrome does not calculate lid angle
// itself, but will rely on the tablet-mode flag that EC sends to decide if
// the device should in tablet mode.
// As it's set in |OnECLidAngleDriverStatusChanged|, which is a callback by
// AccelerometerReader, we make it optional to indicate a lack of value until
// the accelerometer reader is initialized.
std::optional<bool> is_ec_lid_angle_driver_supported_;
// Whether the lid angle can be detected by browser. If it's true, the device
// is a convertible device (both screen acclerometer and keyboard acclerometer
// are available), and doesn't have ChromeOS EC lid angle driver, in this way
// lid angle should be calculated by browser. And if it's false, the device is
// probably a convertible device with ChromeOS EC lid angle driver, or the
// device is either a laptop device or a tablet device (only the screen
// acclerometer is available).
bool can_detect_lid_angle_ = false;
// Tracks time spent in (and out of) tablet mode.
base::Time tablet_mode_usage_interval_start_time_;
base::TimeDelta total_tablet_mode_time_;
base::TimeDelta total_non_tablet_mode_time_;
// Tracks the first time the lid angle was unstable. This is used to suppress
// erroneous accelerometer readings as the lid is nearly opened or closed but
// the accelerometer reports readings that make the lid to appear near fully
// open. (e.g. After closing the lid, the correct angle reading is 0. But the
// accelerometer may report 359.5 degrees which triggers the tablet mode by
// mistake.)
base::TimeTicks first_unstable_lid_angle_time_;
// Source for the current time in base::TimeTicks.
raw_ptr<const base::TickClock> tick_clock_;
// Forces the UI mode to be in tablet or clamsell state. Can be forced via:
// 1) command-line flags, such as `--force-tablet-mode=touch_view` or
// `--force-tablet-mode=clamshell`.
// 2) observing `OnLoginStatusChanged`, since Ui tablet mode is blocked in
// Kiosk.
UiMode forced_ui_mode_ = UiMode::kNone;
// True if the device is physically in a tablet state regardless of the UI
// tablet mode state. The physical tablet state only changes based on device
// events such as lid angle changes, or device getting detached from its base.
bool is_in_tablet_physical_state_ = false;
// Set when tablet mode switch is on. This is used to force tablet mode.
bool tablet_mode_switch_is_on_ = false;
// Tracks when the lid is closed. Used to prevent entering tablet mode.
bool lid_is_closed_ = false;
// True if |lid_angle_| is in the stable range of angle values.
// (See kMinStableAngle and kMaxStableAngle).
bool lid_angle_is_stable_ = false;
// Last computed lid angle.
double lid_angle_ = 0.0f;
// Tracks if the device has an external pointing device. The device will
// not enter tablet mode if this is true.
bool has_external_pointing_device_ = false;
// Tracks if the device has an internal pointing device. The device will not
// enter clamshell mode if both |has_internal_pointing_device_| and
// |has_external_pointing_device_| are false only for tablet-capable devices.
bool has_internal_pointing_device_ = true;
// Set to true temporarily when the tablet mode is enabled/disabled via the
// developer's keyboard shortcut in order to update the visibility of the
// overview tray button, even though internal events are not blocked.
bool force_notify_events_blocking_changed_ = false;
// Counts the app window drag from top in tablet mode.
int app_window_drag_count_ = 0;
// Counts the app window drag from top when splitview is active.
int app_window_drag_in_splitview_count_ = 0;
// Counts of the tab drag from top in tablet mode, includes both non-source
// window and source window drag.
int tab_drag_count_ = 0;
// Counts of the tab drag from top when splitview is active.
int tab_drag_in_splitview_count_ = 0;
// Tracks smoothed accelerometer data over time. This is done when the hinge
// is approaching vertical to remove abrupt acceleration that can lead to
// incorrect calculations of hinge angles.
gfx::Vector3dF base_smoothed_;
gfx::Vector3dF lid_smoothed_;
// Calls RecordLidAngle() periodically.
base::RepeatingTimer record_lid_angle_timer_;
ScopedSessionObserver scoped_session_observer_{this};
std::unique_ptr<aura::WindowOcclusionTracker::ScopedPause>
occlusion_tracker_pauser_;
// Starts when enter/exit tablet mode. Runs ResetPauser to reset the
// occlustion tracker.
base::OneShotTimer occlusion_tracker_reset_timer_;
// Observer to observe the bluetooth devices.
std::unique_ptr<BluetoothDevicesObserver> bluetooth_devices_observer_;
// Observers top windows or animating window during state transition.
std::unique_ptr<DestroyObserver> destroy_observer_;
// The layer that animates duraing tablet mode <-> clamshell
// transition. It's observed to take an action after its animation ends.
raw_ptr<ui::Layer> animating_layer_ = nullptr;
// When in scope, hides the shelf and float containers. Used to temporarily
// hide shelf while taking a screenshot during tablet mode transition (so the
// screenshot does not show the old version of the shelf and floated window in
// the background).
std::unique_ptr<ScopedContainerHider> container_hider_;
// Tracks and record transition smoothness.
std::optional<ui::ThroughputTracker> transition_tracker_;
base::CancelableOnceCallback<void(std::unique_ptr<ui::Layer>)>
screenshot_taken_callback_;
base::CancelableOnceClosure screenshot_set_callback_;
// A layer that is created before an enter tablet mode animations is started,
// and destroyed when the animation is ended. It contains a screenshot of
// everything in the screen rotation container except the top window. It helps
// with animation performance because it fully occludes all windows except the
// animating window for the duration of the animation.
// TODO(sammiequon): See if we can move screenshot and tablet mode transition
// animation related code into a separate class/file.
std::unique_ptr<ui::Layer> screenshot_layer_;
base::ObserverList<TabletModeObserver>::Unchecked tablet_mode_observers_;
TabletModeBehavior tablet_mode_behavior_;
// True if the initial input device setup has been finished. Only after it's
// finished, we'll start monitoring input device add/remove events and respond
// to these events to enter/exit tablet mode accordingly.
bool initial_input_device_set_up_finished_ = false;
base::WeakPtrFactory<TabletModeController> weak_factory_{this};
};
} // namespace ash
#endif // ASH_WM_TABLET_MODE_TABLET_MODE_CONTROLLER_H_