chromium/ash/components/arc/metrics/arc_wm_metrics.cc

// Copyright 2023 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/components/arc/metrics/arc_wm_metrics.h"

#include "ash/public/cpp/app_types_util.h"
#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_state_observer.h"
#include "ash/wm/window_util.h"
#include "base/functional/callback_forward.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/timer/elapsed_timer.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/base/window_state_type.h"
#include "components/exo/shell_surface_base.h"
#include "components/exo/shell_surface_util.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window_observer.h"
#include "ui/base/ui_base_types.h"
#include "ui/display/screen.h"
#include "ui/display/tablet_state.h"

namespace arc {

namespace {

// Histogram of the delay for window maximizing operation.
constexpr char kWindowMaximizedTimeHistogramPrefix[] =
    "Arc.WM.WindowMaximizedDelayTimeV2.";
// Histogram of the delay for window minimizing operation.
constexpr char kWindowMinimizedTimeHistogramPrefix[] =
    "Arc.WM.WindowMinimizedDelayTime.";
// Histogram of the delay for window closing operation.
constexpr char kWindowClosedTimeHistogramPrefix[] =
    "Arc.WM.WindowClosedDelayTimeV2.";
// Histogram of the delay for window state transition when entering into tablet
// mode.
constexpr char kWindowEnterTabletModeTimeHistogramPrefix[] =
    "Arc.WM.WindowEnterTabletModeDelayTimeV2.";
// Histogram of the delay for window state transition when exiting tablet mode.
constexpr char kWindowExitTabletModeTimeHistogramPrefix[] =
    "Arc.WM.WindowExitTabletModeDelayTimeV2.";
// Histogram of the delay for window bounds change when display rotates in
// tablet mode.
constexpr char kWindowRotateTimeHistogramPrefix[] =
    "Arc.WM.WindowRotateDelayTime.";

constexpr char kArcHistogramName[] = "ArcApp";
constexpr char kBrowserHistogramName[] = "Browser";
constexpr char kChromeAppHistogramName[] = "ChromeApp";
constexpr char kSystemAppHistogramName[] = "SystemApp";
constexpr char kCrostiniAppHistogramName[] = "CrostiniApp";

std::string GetAppTypeName(chromeos::AppType app_type) {
  switch (app_type) {
    case chromeos::AppType::ARC_APP:
      return kArcHistogramName;
    case chromeos::AppType::BROWSER:
      return kBrowserHistogramName;
    case chromeos::AppType::CHROME_APP:
      return kChromeAppHistogramName;
    case chromeos::AppType::SYSTEM_APP:
      return kSystemAppHistogramName;
    case chromeos::AppType::CROSTINI_APP:
      return kCrostiniAppHistogramName;
    default:
      return "Others";
  }
}

}  // namespace

// A window state observer that records the delay of window operation (e.g.,
// maximizing and minimizing).
class ArcWmMetrics::WindowStateChangeObserver
    : public ash::WindowStateObserver {
 public:
  WindowStateChangeObserver(aura::Window* window,
                            ui::WindowShowState old_window_show_state,
                            base::OnceClosure callback)
      : window_(window),
        old_window_show_state_(old_window_show_state),
        window_operation_completed_callback_(std::move(callback)) {
    auto* window_state = ash::WindowState::Get(window);
    CHECK(window_state);
    window_state_observation_.Observe(window_state);
  }

  WindowStateChangeObserver(const WindowStateChangeObserver&) = delete;
  WindowStateChangeObserver& operator=(const WindowStateChangeObserver) =
      delete;
  ~WindowStateChangeObserver() override = default;

  // ash::WindowStateObserver:
  void OnPostWindowStateTypeChange(
      ash::WindowState* new_window_state,
      chromeos::WindowStateType old_window_state_type) override {
    // For non-client-controlled windows, if the window remain maximized after
    // leaving tablet mode, `OnPostWindowStateTypeChange` is called with both
    // old state type and new state type equal to `kMaximized`. The histogram
    // does not record data in this case.
    if (old_window_state_type != new_window_state->GetStateType() &&
        old_window_state_type ==
            chromeos::ToWindowStateType(old_window_show_state_)) {
      RecordWindowStateChangeDelay(new_window_state);
    }

    std::move(window_operation_completed_callback_).Run();
  }

 private:
  void RecordWindowStateChangeDelay(ash::WindowState* state) {
    const chromeos::AppType app_type =
        window_->GetProperty(chromeos::kAppTypeKey);
    if (display::Screen::GetScreen()->InTabletMode()) {
      // When entering tablet mode, we only collect the data of visible window.
      if (state->IsMaximized() && window_->IsVisible()) {
        base::UmaHistogramCustomTimes(
            ArcWmMetrics::GetWindowEnterTabletModeTimeHistogramName(app_type),
            window_operation_elapsed_timer_.Elapsed(),
            /*minimum=*/base::Milliseconds(1),
            /*maximum=*/base::Seconds(5), 100);
      }
    } else {
      if (state->IsMaximized()) {
        base::UmaHistogramCustomTimes(
            ArcWmMetrics::GetWindowMaximizedTimeHistogramName(app_type),
            window_operation_elapsed_timer_.Elapsed(),
            /*minimum=*/base::Milliseconds(1),
            /*maximum=*/base::Seconds(3), 100);
      } else if (state->IsMinimized()) {
        base::UmaHistogramCustomTimes(
            ArcWmMetrics::GetWindowMinimizedTimeHistogramName(app_type),
            window_operation_elapsed_timer_.Elapsed(),
            /*minimum=*/base::Milliseconds(1),
            /*maximum=*/base::Seconds(2), 100);
      } else if (state->IsNormalStateType()) {
        base::UmaHistogramCustomTimes(
            ArcWmMetrics::GetWindowExitTabletModeTimeHistogramName(app_type),
            window_operation_elapsed_timer_.Elapsed(),
            /*minimum=*/base::Milliseconds(1),
            /*maximum=*/base::Seconds(5), 100);
      }
    }
  }

  const raw_ptr<aura::Window> window_;
  const ui::WindowShowState old_window_show_state_;

  // Tracks the elapsed time from the window operation happens until the window
  // state is changed.
  base::ElapsedTimer window_operation_elapsed_timer_;

  base::ScopedObservation<ash::WindowState, ash::WindowStateObserver>
      window_state_observation_{this};

  base::OnceClosure window_operation_completed_callback_;
};

// A window observer that records the delay of window closing operation for ARC
// windows.
class ArcWmMetrics::WindowCloseObserver : public aura::WindowObserver {
 public:
  WindowCloseObserver(aura::Window* window, base::OnceClosure callback)
      : window_close_completed_callback_(std::move(callback)) {
    window_observation_.Observe(window);
  }

  WindowCloseObserver(const WindowCloseObserver&) = delete;
  WindowCloseObserver& operator=(const WindowCloseObserver) = delete;
  ~WindowCloseObserver() override = default;

  // aura::WindowObserver:
  void OnWindowDestroyed(aura::Window* window) override {
    RecordWindowCloseDelay();
    std::move(window_close_completed_callback_).Run();
  }

 private:
  void RecordWindowCloseDelay() {
    base::UmaHistogramCustomTimes(
        ArcWmMetrics::GetArcWindowClosedTimeHistogramName(),
        window_close_elapsed_timer_.Elapsed(),
        /*minimum=*/base::Milliseconds(1),
        /*maximum=*/base::Seconds(3), 100);
  }

  // Tracks the elapsed time from the window closing operation happens until the
  // the window is destroyed.
  base::ElapsedTimer window_close_elapsed_timer_;
  base::ScopedObservation<aura::Window, aura::WindowObserver>
      window_observation_{this};

  base::OnceClosure window_close_completed_callback_;
};

// A window observer that records the latency of window bounds change when
// display rotates in tablet mode.
class ArcWmMetrics::WindowRotationObserver : public aura::WindowObserver {
 public:
  WindowRotationObserver(aura::Window* window, base::OnceClosure callback)
      : window_(window),
        window_bounds_changed_completed_callback_(std::move(callback)) {
    window_observation_.Observe(window);
  }

  WindowRotationObserver(const WindowRotationObserver&) = delete;
  WindowRotationObserver& operator=(const WindowRotationObserver) = delete;
  ~WindowRotationObserver() override = default;

  void OnWindowBoundsChanged(aura::Window* window,
                             const gfx::Rect& old_bounds,
                             const gfx::Rect& new_bounds,
                             ui::PropertyChangeReason reason) override {
    if (new_bounds ==
        ash::screen_util::GetMaximizedWindowBoundsInParent(window)) {
      RecordWindowRotateDelay();
      std::move(window_bounds_changed_completed_callback_).Run();
    }
  }

 private:
  void RecordWindowRotateDelay() {
    const chromeos::AppType app_type =
        window_->GetProperty(chromeos::kAppTypeKey);
    base::UmaHistogramCustomTimes(
        ArcWmMetrics::GetWindowRotateTimeHistogramName(app_type),
        window_bounds_change_elapsed_timer_.Elapsed(),
        /*minimum=*/base::Milliseconds(1),
        /*maximum=*/base::Seconds(2), 100);
  }

  const raw_ptr<aura::Window> window_;

  // Tracks the elapsed time from the display rotation happens until the window
  // bounds is changed.
  base::ElapsedTimer window_bounds_change_elapsed_timer_;
  base::ScopedObservation<aura::Window, aura::WindowObserver>
      window_observation_{this};
  base::OnceClosure window_bounds_changed_completed_callback_;
};

ArcWmMetrics::ArcWmMetrics() {
  if (aura::Env::HasInstance()) {
    env_observation_.Observe(aura::Env::GetInstance());
  }

  if (ash::Shell::HasInstance()) {
    shell_observation_.Observe(ash::Shell::Get());
  }
}

ArcWmMetrics::~ArcWmMetrics() = default;

// static
std::string ArcWmMetrics::GetWindowMaximizedTimeHistogramName(
    chromeos::AppType app_type) {
  const std::string app_type_str = GetAppTypeName(app_type);
  return base::StrCat({kWindowMaximizedTimeHistogramPrefix, app_type_str});
}

// static
std::string ArcWmMetrics::GetWindowMinimizedTimeHistogramName(
    chromeos::AppType app_type) {
  const std::string app_type_str = GetAppTypeName(app_type);
  return base::StrCat({kWindowMinimizedTimeHistogramPrefix, app_type_str});
}

// static
std::string ArcWmMetrics::GetArcWindowClosedTimeHistogramName() {
  const std::string arc_app_type_str =
      GetAppTypeName(chromeos::AppType::ARC_APP);
  return base::StrCat({kWindowClosedTimeHistogramPrefix, arc_app_type_str});
}

// static
std::string ArcWmMetrics::GetWindowEnterTabletModeTimeHistogramName(
    chromeos::AppType app_type) {
  const std::string app_type_str = GetAppTypeName(app_type);
  return base::StrCat(
      {kWindowEnterTabletModeTimeHistogramPrefix, app_type_str});
}

// static
std::string ArcWmMetrics::GetWindowExitTabletModeTimeHistogramName(
    chromeos::AppType app_type) {
  const std::string app_type_str = GetAppTypeName(app_type);
  return base::StrCat({kWindowExitTabletModeTimeHistogramPrefix, app_type_str});
}

// static
std::string ArcWmMetrics::GetWindowRotateTimeHistogramName(
    chromeos::AppType app_type) {
  const std::string app_type_str = GetAppTypeName(app_type);
  return base::StrCat({kWindowRotateTimeHistogramPrefix, app_type_str});
}

void ArcWmMetrics::OnWindowInitialized(aura::Window* new_window) {
  chromeos::AppType app_type = new_window->GetProperty(chromeos::kAppTypeKey);
  if (app_type == chromeos::AppType::NON_APP) {
    return;
  }

  if (window_observations_.IsObservingSource(new_window)) {
    return;
  }

  window_observations_.AddObservation(new_window);

  if (app_type == chromeos::AppType::ARC_APP) {
    auto* shell_surface_base = exo::GetShellSurfaceBaseForWindow(new_window);

    // |shell_surface_base| can be null in unit tests.
    if (shell_surface_base) {
      shell_surface_base->set_pre_close_callback(
          base::BindRepeating(&ArcWmMetrics::OnWindowCloseRequested,
                              weak_ptr_factory_.GetWeakPtr(), new_window));
    }
  }
}

void ArcWmMetrics::OnWindowPropertyChanged(aura::Window* window,
                                           const void* key,
                                           intptr_t old) {
  if (key != aura::client::kShowStateKey) {
    return;
  }

  if (state_change_observing_windows_.contains(window)) {
    return;
  }

  if (display::Screen::GetScreen()->InTabletMode()) {
    return;
  }

  const auto new_window_show_state =
      window->GetProperty(aura::client::kShowStateKey);
  const auto old_window_show_state = static_cast<ui::WindowShowState>(old);

  // We do not measure the case that window state is maximized on the app is
  // launched.
  if (new_window_show_state == old_window_show_state) {
    return;
  }

  if (chromeos::ToWindowShowState(
          ash::WindowState::Get(window)->GetStateType()) ==
      new_window_show_state) {
    return;
  }

  const bool from_normal_to_maximized =
      IsNormalWindowStateType(
          chromeos::ToWindowStateType(old_window_show_state)) &&
      new_window_show_state == ui::WindowShowState::SHOW_STATE_MAXIMIZED;
  const bool from_any_to_minimized =
      new_window_show_state == ui::WindowShowState::SHOW_STATE_MINIMIZED;
  if (from_normal_to_maximized || from_any_to_minimized) {
    state_change_observing_windows_.emplace(
        window, std::make_unique<WindowStateChangeObserver>(
                    window, old_window_show_state,
                    base::BindOnce(&ArcWmMetrics::OnOperationCompleted,
                                   base::Unretained(this), window)));
  }

  // The WindowExitTabletModeDelayTime histogram only records data when a
  // maximized window becomes a normal window when exiting tablet mode.
  // Therefore, if a window remains maximized after entering into clamshell
  // mode, we do not need to collect data for that window. This filter is only
  // for client-controlled windows.
  if (exiting_tablet_mode_observing_windows_.contains(window) &&
      ash::WindowState::Get(window)->GetStateType() ==
          chromeos::WindowStateType::kMaximized) {
    exiting_tablet_mode_observing_windows_.erase(window);
  }
}

void ArcWmMetrics::OnWindowDestroying(aura::Window* window) {
  state_change_observing_windows_.erase(window);
  exiting_tablet_mode_observing_windows_.erase(window);
  rotation_observing_windows_.erase(window);
  if (window_observations_.IsObservingSource(window)) {
    window_observations_.RemoveObservation(window);
  }
}

void ArcWmMetrics::OnDisplayTabletStateChanged(display::TabletState state) {
  if (state == display::TabletState::kInClamshellMode) {
    return;
  }

  // After entering tablet mode, we start observing screen rotation.
  if (state == display::TabletState::kInTabletMode) {
    ash::ScreenRotationAnimator* animator =
        ash::RootWindowController::ForWindow(ash::Shell::GetPrimaryRootWindow())
            ->GetScreenRotationAnimator();
    if (animator &&
        !screen_rotation_observations_.IsObservingSource(animator)) {
      screen_rotation_observations_.AddObservation(animator);
    }
    return;
  }

  // When entering or exiting tablet mode, we get the top non floated window and
  // measure the window state change latency for it.
  aura::Window* top_window = ash::window_util::GetTopNonFloatedWindow();
  if (!top_window) {
    return;
  }

  chromeos::WindowStateType window_state_type =
      ash::WindowState::Get(top_window)->GetStateType();

  if (state == display::TabletState::kEnteringTabletMode &&
      IsNormalWindowStateType(window_state_type)) {
    state_change_observing_windows_.emplace(
        top_window,
        std::make_unique<WindowStateChangeObserver>(
            top_window, top_window->GetProperty(aura::client::kShowStateKey),
            base::BindOnce(&ArcWmMetrics::OnOperationCompleted,
                           base::Unretained(this), top_window)));
  } else if (state == display::TabletState::kExitingTabletMode &&
             window_state_type == chromeos::WindowStateType::kMaximized) {
    exiting_tablet_mode_observing_windows_.emplace(
        top_window,
        std::make_unique<WindowStateChangeObserver>(
            top_window, top_window->GetProperty(aura::client::kShowStateKey),
            base::BindOnce(&ArcWmMetrics::OnOperationCompleted,
                           base::Unretained(this), top_window)));
  }
}

void ArcWmMetrics::OnScreenCopiedBeforeRotation() {
  if (!display::Screen::GetScreen()->InTabletMode()) {
    return;
  }

  aura::Window* top_window = ash::window_util::GetTopNonFloatedWindow();
  if (!top_window) {
    return;
  }

  chromeos::WindowStateType window_state_type =
      ash::WindowState::Get(top_window)->GetStateType();
  // We only collect data for the rotation of maximized window.
  if (window_state_type == chromeos::WindowStateType::kMaximized) {
    rotation_observing_windows_.emplace(
        top_window,
        std::make_unique<WindowRotationObserver>(
            top_window, base::BindOnce(&ArcWmMetrics::OnWindowRotationCompleted,
                                       base::Unretained(this), top_window)));
  }
}

void ArcWmMetrics::OnScreenRotationAnimationFinished(
    ash::ScreenRotationAnimator* animator,
    bool canceled) {
  rotation_observing_windows_.clear();
}

void ArcWmMetrics::OnRootWindowWillShutdown(aura::Window* root_window) {
  if (auto* const animator = ash::RootWindowController::ForWindow(root_window)
                                 ->GetScreenRotationAnimator();
      animator && screen_rotation_observations_.IsObservingSource(animator)) {
    screen_rotation_observations_.RemoveObservation(animator);
  }
}

void ArcWmMetrics::OnShellDestroying() {
  shell_observation_.Reset();
  screen_rotation_observations_.RemoveAllObservations();
}

void ArcWmMetrics::OnOperationCompleted(aura::Window* window) {
  state_change_observing_windows_.erase(window);
  exiting_tablet_mode_observing_windows_.erase(window);
}

void ArcWmMetrics::OnWindowRotationCompleted(aura::Window* window) {
  rotation_observing_windows_.erase(window);
}

void ArcWmMetrics::OnWindowCloseRequested(aura::Window* window) {
  close_observing_windows_.emplace(
      window, std::make_unique<WindowCloseObserver>(
                  window, base::BindOnce(&ArcWmMetrics::OnWindowCloseCompleted,
                                         base::Unretained(this), window)));
}

void ArcWmMetrics::OnWindowCloseCompleted(aura::Window* window) {
  close_observing_windows_.erase(window);
}

}  // namespace arc