// Copyright 2021 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/splitview/split_view_metrics_controller.h"
#include <vector>
#include "ash/root_window_controller.h"
#include "ash/root_window_settings.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/switchable_windows.h"
#include "ash/wm/window_positioning_utils.h"
#include "ash/wm/window_restore/window_restore_controller.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_metrics.h"
#include "base/check_op.h"
#include "base/containers/adapters.h"
#include "base/containers/contains.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/time/time.h"
#include "chromeos/ui/base/display_util.h"
#include "chromeos/ui/base/window_state_type.h"
#include "components/app_restore/window_info.h"
#include "components/app_restore/window_properties.h"
#include "components/prefs/pref_service.h"
#include "ui/aura/env.h"
#include "ui/display/screen.h"
#include "ui/display/tablet_state.h"
#include "ui/wm/public/activation_client.h"
namespace ash {
namespace {
// Histogram of the device UI mode when entering split view.
constexpr char kSplitViewEntryPointDeviceUIModeHistogram[] =
"Ash.SplitView.EntryPoint.DeviceUIMode";
// Histogram of the device orientation when entering split view.
constexpr char kSplitViewEntryPointDeviceOrientationHistogram[] =
"Ash.SplitView.EntryPoint.DeviceOrientation";
// Histogram of the device orientation when using split view.
constexpr char kSplitViewDeviceOrientationPrefix[] =
"Ash.SplitView.DeviceOrientation";
// Histogram of the device orientation changes when using split view.
constexpr char kOrientationInSplitViewHistogram[] =
"Ash.SplitView.OrientationInSplitView";
// Histogram of the engagement time in clamshell split view.
constexpr char kTimeInSplitScreenClamshellHistogram[] =
"Ash.SplitView.TimeInSplitScreen.ClamshellMode";
// Histogram of the engagement time in tablet split view.
constexpr char kTimeInSplitScreenTabletHistogram[] =
"Ash.SplitView.TimeInSplitScreen.TabletMode";
// Histogram of the engagement time in multi-display clamshell split view.
constexpr char kTimeInMultiDisplaySplitScreenClamshellHistogram[] =
"Ash.SplitView.TimeInMultiDisplaySplitScreen.ClamshellMode";
// Histogram of the engagement time in multi-display tablet split view.
constexpr char kTimeInMultiDisplaySplitScreenTabletHistogram[] =
"Ash.SplitView.TimeInMultiDisplaySplitScreen.TabletMode";
// Histogram of the number of resizing window operations in clamshell split
// view.
constexpr char kSplitViewResizeWindowCountClamshellHistogram[] =
"Ash.SplitView.ResizeWindowCount.ClamshellMode";
// Histogram of the number of resizing window operations in tablet split view.
constexpr char kSplitViewResizeWindowCountTabletHistogram[] =
"Ash.SplitView.ResizeWindowCount.TabletMode";
// Histogram of the number of swapping window operations in split view.
constexpr char kSplitViewSwapWindowCountHistogram[] =
"Ash.SplitView.SwapWindowCount";
constexpr base::TimeTicks kInvalidTime = base::TimeTicks::Max();
// Start time of clamshell and tablet multi-display split view.
base::TimeTicks g_clamshell_multi_display_split_view_start_time;
base::TimeTicks g_tablet_multi_display_split_view_start_time;
// An accumulator of clamshell multi-display split view engagement time. When
// the clamshell split view with two windows snapped on both sides is paused
// (the definition of "pause" is defined in the comments at the beginning of the
// the header file), accumulate the current engagement time period.
int64_t g_clamshell_multi_display_split_view_time_ms;
bool IsRecordingClamshellMultiDisplaySplitView() {
return g_clamshell_multi_display_split_view_start_time != kInvalidTime;
}
bool IsRecordingTabletMultiDisplaySplitView() {
return g_tablet_multi_display_split_view_start_time != kInvalidTime;
}
// Number of root windows in split view.
int NumRootWindowsInSplitViewRecording() {
auto root_windows = Shell::GetAllRootWindows();
return base::ranges::count_if(root_windows, [](aura::Window* root_window) {
return SplitViewController::Get(root_window)
->split_view_metrics_controller()
->in_split_view_recording();
});
}
bool InTabletMode() {
return display::Screen::GetScreen()->InTabletMode();
}
bool TopTwoVisibleWindowsBothSnapped(
const std::vector<raw_ptr<aura::Window, VectorExperimental>>& windows) {
int windows_size = windows.size();
if (windows_size < 2)
return false;
// Check if there are snapped windows on both sides without hidden by other
// windows. The topmost window is at the end of the list.
WindowState* top_snap_window_state = WindowState::Get(windows.back());
if (!top_snap_window_state->IsSnapped())
return false;
for (aura::Window* window : base::Reversed(windows)) {
// Skip the top one.
if (window == windows.back())
continue;
auto* window_state = WindowState::Get(window);
// Skip the invisible windows.
if (!window->IsVisible())
continue;
if (!window_state->IsSnapped())
return false;
if (window_state->GetStateType() == top_snap_window_state->GetStateType()) {
continue;
} else {
return true;
}
}
return false;
}
// Appends the proper suffix to |prefix| based on whether the device is in
// tablet mode or not.
std::string GetHistogramNameWithDeviceUIMode(std::string prefix) {
return prefix.append(InTabletMode() ? ".TabletMode" : ".ClamshellMode");
}
SplitViewMetricsController::DeviceOrientation GetDeviceOrientation(
const display::Display& display) {
return display.is_landscape()
? SplitViewMetricsController::DeviceOrientation::kLandscape
: SplitViewMetricsController::DeviceOrientation::kPortrait;
}
// Records the pref value of `kSnapWindowSuggestions` at the time a window is
// snapped.
void MaybeRecordSnapWindowSuggestions(
WindowSnapActionSource snap_action_source) {
if (!CanSnapActionSourceStartFasterSplitView(snap_action_source)) {
return;
}
PrefService* pref_service =
Shell::Get()->session_controller()->GetActivePrefService();
if (!pref_service) {
return;
}
base::UmaHistogramBoolean(
BuildSnapWindowSuggestionsHistogramName(snap_action_source),
pref_service->GetBoolean(prefs::kSnapWindowSuggestions));
}
} // namespace
// static
SplitViewMetricsController* SplitViewMetricsController::Get(
aura::Window* window) {
DCHECK(window);
DCHECK(window->GetRootWindow());
auto* root_window_controller = RootWindowController::ForWindow(window);
DCHECK(root_window_controller);
return root_window_controller->split_view_controller()
->split_view_metrics_controller();
}
SplitViewMetricsController::SplitViewMetricsController(
SplitViewController* split_view_controller)
: split_view_controller_(split_view_controller) {
split_view_controller_->AddObserver(this);
Shell::Get()->activation_client()->AddObserver(this);
auto* desks_controller = Shell::Get()->desks_controller();
desks_controller->AddObserver(this);
current_desk_ = desks_controller->active_desk();
aura::Env::GetInstance()->AddObserver(this);
const display::Display display =
display::Screen::GetScreen()->GetDisplayNearestWindow(
split_view_controller->root_window());
orientation_ = GetDeviceOrientation(display);
ResetTimeAndCounter();
}
SplitViewMetricsController::~SplitViewMetricsController() {
ClearObservedWindows();
split_view_controller_->RemoveObserver(this);
Shell::Get()->activation_client()->RemoveObserver(this);
Shell::Get()->desks_controller()->RemoveObserver(this);
aura::Env::GetInstance()->RemoveObserver(this);
}
void SplitViewMetricsController::OnSplitViewStateChanged(
SplitViewController::State previous_state,
SplitViewController::State state) {
if (previous_state == state)
return;
if (previous_state == SplitViewController::State::kNoSnap)
StartRecordSplitViewMetrics();
else if (state == SplitViewController::State::kNoSnap)
StopRecordSplitViewMetrics();
}
void SplitViewMetricsController::OnSplitViewWindowResized() {
DCHECK(split_view_controller_->InSplitViewMode());
if (split_view_controller_->InClamshellSplitViewMode())
clamshell_resize_count_ += 1;
else
tablet_resize_count_ += 1;
}
void SplitViewMetricsController::OnSplitViewWindowSwapped() {
DCHECK(split_view_controller_->InSplitViewMode());
swap_count_ += 1;
// Decreases the counter by 2, since swapping windows will trigger resizing
// window twice.
if (split_view_controller_->InClamshellSplitViewMode())
clamshell_resize_count_ -= 2;
else
tablet_resize_count_ -= 2;
}
void SplitViewMetricsController::OnDisplayMetricsChanged(
const display::Display& display,
uint32_t changed_metrics) {
if (!(changed_metrics &
display::DisplayObserver::DisplayMetric::DISPLAY_METRIC_ROTATION)) {
return;
}
// Do nothing if the split view does not belongs to the modified display.
if (GetRootWindowSettings(split_view_controller_->root_window())
->display_id != display.id()) {
return;
}
const DeviceOrientation orientation = GetDeviceOrientation(display);
if (orientation_ == orientation)
return;
orientation_ = orientation;
// Reports change of the display orientation.
if (in_split_view_recording_) {
base::UmaHistogramEnumeration(kOrientationInSplitViewHistogram,
orientation_);
ReportDeviceUIModeAndOrientationHistogram();
}
}
void SplitViewMetricsController::OnDisplayTabletStateChanged(
display::TabletState state) {
switch (state) {
case display::TabletState::kEnteringTabletMode:
case display::TabletState::kExitingTabletMode:
break;
case display::TabletState::kInTabletMode:
OnTabletModeStarted();
break;
case display::TabletState::kInClamshellMode:
OnTabletModeEnded();
break;
}
}
void SplitViewMetricsController::OnWindowParentChanged(aura::Window* window,
aura::Window* parent) {
// Stop observing the window when it is moved to another desk. If the restored
// window is parented to current desk, observe its window state change.
if (parent && desks_util::IsDeskContainer(parent)) {
if (parent->GetId() != current_desk_->container_id()) {
RemoveObservedWindow(window);
} else if (base::Contains(no_state_observed_windows_, window)) {
WindowState::Get(window)->AddObserver(this);
no_state_observed_windows_.erase(window);
}
// If the top two windows snapped on both sides in clamshell mode, moving
// one of the snapped windows to another desk may end the split view. If
// there are two windows snapped on both sides with another unsnapped window
// on top in clamshell mode, moving the unsnapped window to another desk may
// start split view.
MaybeStartOrEndRecordBothSnappedClamshellSplitView();
}
}
void SplitViewMetricsController::OnResizeLoopEnded(aura::Window* window) {
// Only report window resizing if it is in the split view with two windows
// snapped on both sides.
if (!in_split_view_recording_ || split_view_controller_->InSplitViewMode())
return;
clamshell_resize_count_ += 1;
}
void SplitViewMetricsController::OnWindowDestroyed(aura::Window* window) {
RemoveObservedWindow(window);
// If the top two windows snapped on both sides in clamshell mode,
// destroying one of the snapped windows may end the split view. If there
// are two windows snapped on both sides with another unsnapped window
// on top in clamshell mode, destroying the unsnapped window may start split
// view.
MaybeStartOrEndRecordBothSnappedClamshellSplitView();
}
void SplitViewMetricsController::OnWindowRemovingFromRootWindow(
aura::Window* window,
aura::Window* new_root) {
// Stop observing the window if it is removing from current root window.
RemoveObservedWindow(window);
// If the top two windows snapped on both sides in clamshell mode,
// moving one of the snapped windows to another display may end the split
// view. If there are two windows snapped on both sides with another unsnapped
// window on top in clamshell mode, moving the unsnapped window to another
// display may start split view.
MaybeStartOrEndRecordBothSnappedClamshellSplitView();
// Add the window to the new root window's split view metrics controller. It
// may make the new root window start or end split view metrics recording.
if (new_root) {
auto* target_split_view_metrics_controller =
SplitViewController::Get(new_root)->split_view_metrics_controller();
DCHECK(target_split_view_metrics_controller);
if (!target_split_view_metrics_controller->IsObservingWindow(window)) {
target_split_view_metrics_controller->AddObservedWindow(window);
target_split_view_metrics_controller
->MaybeStartOrEndRecordBothSnappedClamshellSplitView();
}
}
}
void SplitViewMetricsController::OnPostWindowStateTypeChange(
WindowState* window_state,
chromeos::WindowStateType old_type) {
MaybeStartOrEndRecordSnapTwoWindowsDuration(window_state);
MaybeStartOrEndRecordMinimizeTwoWindowsDuration(window_state, old_type);
// We only care if a window is snapped or unsnapped.
bool is_snapped = window_state->IsSnapped();
if (is_snapped) {
MaybeRecordSnapWindowSuggestions(
window_state->snap_action_source().value_or(
WindowSnapActionSource::kNotSpecified));
}
bool was_snapped = chromeos::IsSnappedWindowStateType(old_type);
if (is_snapped == was_snapped)
return;
if (was_snapped &&
chromeos::IsSnappedWindowStateType(first_closed_state_type_) &&
old_type != first_closed_state_type_) {
// If a window in the opposite side of `first_closed_state_type_` gets
// unsnapped, record the max duration to indicate a second snapped window
// was never closed after the first window.
RecordCloseTwoWindowsDuration(kSequentialSnapActionMaxTime);
}
MaybeStartOrEndRecordBothSnappedClamshellSplitView();
}
void SplitViewMetricsController::OnWindowActivated(ActivationReason reason,
aura::Window* gained_active,
aura::Window* lost_active) {
// Reorder the observed windows.
AddOrStackWindowOnTop(gained_active);
MaybeStartOrEndRecordBothSnappedClamshellSplitView();
}
void SplitViewMetricsController::OnDeskActivationChanged(
const Desk* activated,
const Desk* deactivated) {
// When switching desks, ends the split view and updates observed windows.
StopRecordSplitViewMetrics();
current_desk_ = Shell::Get()->desks_controller()->active_desk();
InitObservedWindowsOnActiveDesk();
// Check if the new desk is in clamshell split view with two windows snapped
// on both sides.
MaybeStartOrEndRecordBothSnappedClamshellSplitView();
}
void SplitViewMetricsController::OnWindowInitialized(aura::Window* window) {
int32_t* activation_index =
window->GetProperty(app_restore::kActivationIndexKey);
if (!activation_index)
return;
app_restore::WindowInfo* window_info =
window->GetProperty(app_restore::kWindowInfoKey);
if (!window_info)
return;
// Check if the recovered window belongs to the same root window.
// Note: The display id saved in window_info has no value. Need to use the
// restore bounds/
if (!window_info->current_bounds.has_value() ||
!display::Screen::GetScreen()
->GetDisplayNearestWindow(split_view_controller_->root_window())
.work_area()
.Contains(window_info->current_bounds.value())) {
return;
}
// Check if the recovered window is in the current desk.
if (!window_info->desk_guid.is_valid() ||
window_info->desk_guid != current_desk_->uuid()) {
return;
}
// Insert the window in the `observed_windows_` list according to its
// activation index key. Since the window is not parented at this stage, the
// `WindowStateObserver` will be added later in `OnWindowParentChanged`.
window->AddObserver(this);
no_state_observed_windows_.insert(window);
observed_windows_.insert(WindowRestoreController::GetWindowToInsertBefore(
window, observed_windows_),
window);
}
void SplitViewMetricsController::StartRecordSplitViewMetrics() {
if (in_split_view_recording_)
return;
// If the split view is started with a snapped window next to the overview,
// and there is only one window (no overview items), the overview will end
// immediately so does the split view. We won't record this case.
if (split_view_controller_->InClamshellSplitViewMode() &&
Shell::Get()
->mru_window_tracker()
->BuildMruWindowList(DesksMruType::kActiveDesk)
.size() == 1) {
return;
}
bool in_clamshell = !InTabletMode();
if (in_clamshell)
StartRecordClamshellSplitView();
else
StartRecordTabletSplitView();
in_split_view_recording_ = true;
// The starting of this split view makes the number of root windows in split
// view become two, which means the multi-display split view just started.
if (NumRootWindowsInSplitViewRecording() == 2) {
if (in_clamshell)
StartRecordClamshellMultiDisplaySplitView();
else
StartRecordTabletMultiDisplaySplitView();
}
base::UmaHistogramEnumeration(kSplitViewEntryPointDeviceOrientationHistogram,
orientation_);
base::UmaHistogramEnumeration(
kSplitViewEntryPointDeviceUIModeHistogram,
in_clamshell ? DeviceUIMode::kClamshell : DeviceUIMode::kTablet);
}
void SplitViewMetricsController::StopRecordSplitViewMetrics() {
if (!in_split_view_recording_)
return;
bool is_recording_clamshell_metrics = IsRecordingClamshellMetrics();
if (is_recording_clamshell_metrics) {
// If the split view is started with a snapped window next to the overview,
// the the user activate a window by clicking the overview item, the window
// will snap to the other side. Therefore, stacks the window on top.
if (auto* to_be_activated_window =
split_view_controller_->to_be_activated_window()) {
AddOrStackWindowOnTop(to_be_activated_window);
}
// Do not end if there are still two windows snapped on both sides on top.
if (TopTwoVisibleWindowsBothSnapped(observed_windows_))
return;
StopRecordClamshellSplitView();
} else {
StopRecordTabletSplitView();
}
base::UmaHistogramCounts100(kSplitViewSwapWindowCountHistogram, swap_count_);
in_split_view_recording_ = false;
// The ending of this split view makes the number of root windows in split
// view become one, which means the multi-display split view just ended.
if (NumRootWindowsInSplitViewRecording() == 1) {
if (is_recording_clamshell_metrics)
StopRecordClamshellMultiDisplaySplitView();
else
StopRecordTabletMultiDisplaySplitView();
}
ResetTimeAndCounter();
}
bool SplitViewMetricsController::IsObservingWindow(aura::Window* window) const {
return base::Contains(observed_windows_, window);
}
void SplitViewMetricsController::AddObservedWindow(aura::Window* window) {
window->AddObserver(this);
WindowState::Get(window)->AddObserver(this);
observed_windows_.emplace_back(window);
}
void SplitViewMetricsController::RemoveObservedWindow(aura::Window* window) {
if (window->is_destroying()) {
MaybeStartOrEndRecordCloseTwoWindowsDuration(window);
}
if (window == first_snapped_window_) {
if (window->is_destroying()) {
// If `first_snapped_window_` was destroyed, record the max duration to
// indicate a second window was never snapped on the opposite side.
RecordSnapTwoWindowsDuration(kSequentialSnapActionMaxTime);
}
first_snapped_window_ = nullptr;
}
if (first_minimized_window_state_ &&
window == first_minimized_window_state_->window()) {
if (window->is_destroying()) {
RecordMinimizeTwoWindowsDuration(kSequentialSnapActionMaxTime);
}
first_minimized_window_state_ = nullptr;
}
if (std::erase(observed_windows_, window)) {
WindowState::Get(window)->RemoveObserver(this);
window->RemoveObserver(this);
}
}
void SplitViewMetricsController::AddOrStackWindowOnTop(aura::Window* window) {
// We only observe the activable windows on active desk of the root window
// attached by |split_view_controller_|.
if (!window)
return;
if (window->GetRootWindow() != split_view_controller_->root_window())
return;
aura::Window* parent = window->parent();
if (!parent || !IsSwitchableContainer(parent))
return;
const int parent_id = parent->GetId();
if (desks_util::IsDeskContainerId(parent_id) &&
parent_id != current_desk_->container_id()) {
return;
}
if (!CanIncludeWindowInMruList(window))
return;
auto iter = base::ranges::find(observed_windows_, window);
if (iter == observed_windows_.end()) {
AddObservedWindow(window);
} else {
observed_windows_.erase(iter);
observed_windows_.emplace_back(window);
}
}
void SplitViewMetricsController::InitObservedWindowsOnActiveDesk() {
ClearObservedWindows();
auto windows =
current_desk_
->GetDeskContainerForRoot(split_view_controller_->root_window())
->children();
for (aura::Window* window : windows) {
if (!CanIncludeWindowInMruList(window))
continue;
AddObservedWindow(window);
}
}
void SplitViewMetricsController::ClearObservedWindows() {
while (!observed_windows_.empty()) {
RemoveObservedWindow(observed_windows_.back());
}
}
void SplitViewMetricsController::
MaybeStartOrEndRecordBothSnappedClamshellSplitView() {
if (InTabletMode() || split_view_controller_->InSplitViewMode()) {
return;
}
bool both_snapped = TopTwoVisibleWindowsBothSnapped(observed_windows_);
if (!in_split_view_recording_ && both_snapped) {
StartRecordSplitViewMetrics();
} else if (in_split_view_recording_ && !both_snapped) {
StopRecordSplitViewMetrics();
}
}
bool SplitViewMetricsController::
MaybePauseRecordBothSnappedClamshellSplitView() {
if (InTabletMode() || split_view_controller_->InSplitViewMode()) {
return false;
}
if (observed_windows_.size() < 3)
return false;
// Find the topmost unsnapped visible window.
auto iter = observed_windows_.end() - 1;
auto begin_iter = observed_windows_.begin();
for (; iter != begin_iter; iter--) {
if (!(*iter)->IsVisible())
continue;
if (WindowState::Get(*iter)->IsSnapped())
return false;
else
break;
}
if (iter == begin_iter)
return false;
return TopTwoVisibleWindowsBothSnapped(
std::vector<raw_ptr<aura::Window, VectorExperimental>>(begin_iter, iter));
}
void SplitViewMetricsController::RecordSnapTwoWindowsDuration(
const base::TimeDelta& elapsed_time) {
base::UmaHistogramCustomTimes(kSnapTwoWindowsDurationHistogramName,
/*sample=*/elapsed_time,
kSequentialSnapActionMinTime,
kSequentialSnapActionMaxTime, /*buckets=*/100);
first_snapped_window_ = nullptr;
first_snapped_time_ = base::TimeTicks();
}
void SplitViewMetricsController::RecordMinimizeTwoWindowsDuration(
const base::TimeDelta& elapsed_time) {
base::UmaHistogramCustomTimes(kMinimizeTwoWindowsDurationHistogramName,
/*sample=*/elapsed_time,
kSequentialSnapActionMinTime,
kSequentialSnapActionMaxTime, /*buckets=*/100);
first_minimized_window_state_ = nullptr;
first_minimized_time_ = base::TimeTicks();
}
void SplitViewMetricsController::RecordCloseTwoWindowsDuration(
const base::TimeDelta& elapsed_time) {
base::UmaHistogramCustomTimes(kCloseTwoWindowsDurationHistogramName,
/*sample=*/elapsed_time,
kSequentialSnapActionMinTime,
kSequentialSnapActionMaxTime, /*buckets=*/100);
// Reset `first_closed_state_type_` to kDefault to stop recording.
first_closed_state_type_ = chromeos::WindowStateType::kDefault;
first_closed_time_ = base::TimeTicks();
}
void SplitViewMetricsController::MaybeStartOrEndRecordSnapTwoWindowsDuration(
WindowState* window_state) {
// If `first_snapped_window_` is no longer snapped, record the max duration to
// indicate a second window was never snapped on the opposite side.
if (first_snapped_window_ &&
!WindowState::Get(first_snapped_window_)->IsSnapped()) {
// Any state type change can change `first_snapped_window_`'s state type
// (i.e. float). This must be reset before we check `first_snapped_window_`
// below.
RecordSnapTwoWindowsDuration(kSequentialSnapActionMaxTime);
}
if (window_state->IsSnapped()) {
if (first_snapped_window_ && !first_snapped_time_.is_null() &&
window_state->window() != first_snapped_window_ &&
window_state->GetStateType() ==
ToWindowStateType(GetOppositeSnapType(first_snapped_window_))) {
// If this is a different window that got snapped on the opposite side,
// record the duration since `first_snapped_time_`.
RecordSnapTwoWindowsDuration(base::TimeTicks::Now() -
first_snapped_time_);
return;
}
// Else start recording. If the same window gets snapped again, this will
// restart recording.
first_snapped_window_ = window_state->window();
first_snapped_time_ = base::TimeTicks::Now();
return;
}
}
void SplitViewMetricsController::
MaybeStartOrEndRecordMinimizeTwoWindowsDuration(
WindowState* window_state,
chromeos::WindowStateType old_type) {
const bool is_minimized = window_state->IsMinimized();
if (is_minimized && chromeos::IsSnappedWindowStateType(old_type)) {
if (first_minimized_window_state_ && !first_minimized_time_.is_null()) {
// No need to check if `first_minimized_window_state_` is the same as
// `window_state`, since if it changes state it would no longer be
// minimized, and would fall through to record the max duration below.
RecordMinimizeTwoWindowsDuration(base::TimeTicks::Now() -
first_minimized_time_);
return;
}
first_minimized_window_state_ = window_state;
first_minimized_time_ = base::TimeTicks::Now();
return;
}
if (window_state == first_minimized_window_state_ && !is_minimized &&
!window_state->IsSnapped()) {
// If the first window is no longer minimized or snapped, record the max
// duration to indicate no other window was snapped then minimized.
RecordMinimizeTwoWindowsDuration(kSequentialSnapActionMaxTime);
}
}
void SplitViewMetricsController::MaybeStartOrEndRecordCloseTwoWindowsDuration(
aura::Window* window) {
if (auto* window_state = WindowState::Get(window);
window_state && window_state->IsSnapped()) {
if (!chromeos::IsSnappedWindowStateType(first_closed_state_type_)) {
// If `first_closed_state_type_` is reset to kDefault, start recording.
first_closed_state_type_ = window_state->GetStateType();
first_closed_time_ = base::TimeTicks::Now();
return;
}
// If `window` has the opposite state type of `first_closed_state_type_`,
// record the duration.
if (ToWindowStateType(GetOppositeSnapType(window)) ==
first_closed_state_type_ &&
!first_closed_time_.is_null()) {
RecordCloseTwoWindowsDuration(base::TimeTicks::Now() -
first_closed_time_);
}
}
}
void SplitViewMetricsController::ResetTimeAndCounter() {
clamshell_split_view_start_time_ = kInvalidTime;
tablet_split_view_start_time_ = kInvalidTime;
clamshell_resize_count_ = 0;
tablet_resize_count_ = 0;
swap_count_ = 0;
}
void SplitViewMetricsController::OnTabletModeStarted() {
// If it has been in split view and recording clamshell mode metrics, stop
// recording clamshell mode metrics and start to record tablet mode metrics.
if (in_split_view_recording_ && IsRecordingClamshellMetrics()) {
StopRecordClamshellSplitView();
StartRecordTabletSplitView();
if (NumRootWindowsInSplitViewRecording() > 1 &&
IsRecordingClamshellMultiDisplaySplitView()) {
StopRecordClamshellMultiDisplaySplitView();
StartRecordTabletMultiDisplaySplitView();
}
}
}
void SplitViewMetricsController::OnTabletModeEnded() {
// If it has been in split view and recording tablet mode metrics, stop
// recording tablet mode metrics and start to record clamshell mode metrics.
if (in_split_view_recording_ && IsRecordingTabletMetrics()) {
StopRecordTabletSplitView();
StartRecordClamshellSplitView();
if (NumRootWindowsInSplitViewRecording() > 1 &&
IsRecordingTabletMultiDisplaySplitView()) {
StopRecordTabletMultiDisplaySplitView();
StartRecordClamshellMultiDisplaySplitView();
}
}
}
bool SplitViewMetricsController::IsRecordingClamshellMetrics() const {
return clamshell_split_view_start_time_ != kInvalidTime;
}
bool SplitViewMetricsController::IsRecordingTabletMetrics() const {
return tablet_split_view_start_time_ != kInvalidTime;
}
void SplitViewMetricsController::StartRecordClamshellSplitView() {
clamshell_split_view_start_time_ = base::TimeTicks::Now();
ReportDeviceUIModeAndOrientationHistogram();
}
void SplitViewMetricsController::StopRecordClamshellSplitView() {
DCHECK_NE(clamshell_split_view_start_time_, kInvalidTime);
// Accumulate the engagement time.
clamshell_split_view_time_ +=
(base::TimeTicks::Now() - clamshell_split_view_start_time_)
.InMicroseconds();
clamshell_split_view_start_time_ = kInvalidTime;
// If pauses, do not emit the records.
if (MaybePauseRecordBothSnappedClamshellSplitView())
return;
base::UmaHistogramLongTimes(kTimeInSplitScreenClamshellHistogram,
base::Milliseconds(clamshell_split_view_time_));
base::UmaHistogramCounts100(kSplitViewResizeWindowCountClamshellHistogram,
clamshell_resize_count_);
clamshell_split_view_time_ = 0;
clamshell_resize_count_ = 0;
}
void SplitViewMetricsController::StartRecordTabletSplitView() {
tablet_split_view_start_time_ = base::TimeTicks::Now();
ReportDeviceUIModeAndOrientationHistogram();
}
void SplitViewMetricsController::StopRecordTabletSplitView() {
DCHECK_NE(tablet_split_view_start_time_, kInvalidTime);
base::UmaHistogramLongTimes(
kTimeInSplitScreenTabletHistogram,
base::TimeTicks::Now() - tablet_split_view_start_time_);
tablet_split_view_start_time_ = kInvalidTime;
base::UmaHistogramCounts100(kSplitViewResizeWindowCountTabletHistogram,
tablet_resize_count_);
tablet_resize_count_ = 0;
}
void SplitViewMetricsController::StartRecordClamshellMultiDisplaySplitView() {
g_clamshell_multi_display_split_view_start_time = base::TimeTicks::Now();
ReportDeviceUIModeAndOrientationHistogram();
}
void SplitViewMetricsController::StopRecordClamshellMultiDisplaySplitView() {
DCHECK_NE(g_clamshell_multi_display_split_view_start_time, kInvalidTime);
// Accumulate the engagement time.
g_clamshell_multi_display_split_view_time_ms =
(base::TimeTicks::Now() - g_clamshell_multi_display_split_view_start_time)
.InMilliseconds();
g_clamshell_multi_display_split_view_start_time = kInvalidTime;
// If pauses, do not emit the records.
if (MaybePauseRecordBothSnappedClamshellSplitView())
return;
base::UmaHistogramLongTimes(
kTimeInMultiDisplaySplitScreenClamshellHistogram,
base::Milliseconds(g_clamshell_multi_display_split_view_time_ms));
g_clamshell_multi_display_split_view_time_ms = 0;
}
void SplitViewMetricsController::StartRecordTabletMultiDisplaySplitView() {
g_tablet_multi_display_split_view_start_time = base::TimeTicks::Now();
ReportDeviceUIModeAndOrientationHistogram();
}
void SplitViewMetricsController::StopRecordTabletMultiDisplaySplitView() {
DCHECK_NE(g_tablet_multi_display_split_view_start_time, kInvalidTime);
base::UmaHistogramLongTimes(
kTimeInMultiDisplaySplitScreenTabletHistogram,
base::TimeTicks::Now() - g_tablet_multi_display_split_view_start_time);
g_tablet_multi_display_split_view_start_time = kInvalidTime;
}
void SplitViewMetricsController::ReportDeviceUIModeAndOrientationHistogram() {
base::UmaHistogramEnumeration(
GetHistogramNameWithDeviceUIMode(kSplitViewDeviceOrientationPrefix),
orientation_);
}
} // namespace ash