chromium/ash/capture_mode/capture_mode_test_util.cc

// Copyright 2022 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/capture_mode/capture_mode_test_util.h"

#include "ash/accessibility/a11y_feature_type.h"
#include "ash/accessibility/accessibility_controller.h"
#include "ash/accessibility/autoclick/autoclick_controller.h"
#include "ash/capture_mode/capture_mode_bar_view.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_mode_session.h"
#include "ash/capture_mode/capture_mode_session_test_api.h"
#include "ash/capture_mode/capture_mode_source_view.h"
#include "ash/capture_mode/capture_mode_type_view.h"
#include "ash/capture_mode/fake_video_source_provider.h"
#include "ash/capture_mode/test_capture_mode_delegate.h"
#include "ash/public/cpp/capture_mode/capture_mode_test_api.h"
#include "ash/public/cpp/projector/projector_controller.h"
#include "ash/public/cpp/projector/projector_new_screencast_precondition.h"
#include "ash/public/cpp/projector/projector_session.h"
#include "ash/public/cpp/projector/speech_recognition_availability.h"
#include "ash/shell.h"
#include "ash/style/icon_button.h"
#include "ash/style/pill_button.h"
#include "ash/style/tab_slider.h"
#include "ash/system/accessibility/autoclick_menu_bubble_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/safe_base_name.h"
#include "base/location.h"
#include "base/memory/ref_counted_memory.h"
#include "base/ranges/algorithm.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/threading/scoped_blocking_call.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/ime/constants.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/image/image.h"
#include "ui/views/view.h"
#include "ui/views/view_observer.h"
#include "ui/wm/core/coordinate_conversion.h"

namespace ash {

namespace {

constexpr char kScreenCaptureNotificationId[] = "capture_mode_notification";
constexpr char kDefaultCameraDisplayName[] = "Default Cam";

// Dispatch the simulated virtual key event to the WindowEventDispatcher.
void DispatchVKEvent(ui::test::EventGenerator* event_generator,
                     bool is_press,
                     ui::KeyboardCode key_code,
                     int flags,
                     int source_device_id) {
  ui::EventType type =
      is_press ? ui::EventType::kKeyPressed : ui::EventType::kKeyReleased;
  ui::KeyEvent keyev(type, key_code, flags);

  keyev.SetProperties({{
      ui::kPropertyFromVK,
      std::vector<uint8_t>(ui::kPropertyFromVKSize),
  }});
  keyev.set_source_device_id(source_device_id);
  event_generator->Dispatch(&keyev);
}

}  // namespace

CaptureModeController* StartCaptureSession(CaptureModeSource source,
                                           CaptureModeType type) {
  auto* controller = CaptureModeController::Get();
  controller->SetSource(source);
  controller->SetType(type);
  controller->Start(CaptureModeEntryType::kQuickSettings);
  CHECK(controller->IsActive());
  return controller;
}

TestCaptureModeDelegate* GetTestDelegate() {
  return static_cast<TestCaptureModeDelegate*>(
      CaptureModeController::Get()->delegate_for_testing());
}

void ClickOnView(const views::View* view,
                 ui::test::EventGenerator* event_generator) {
  DCHECK(view);
  DCHECK(event_generator);

  const gfx::Point view_center = view->GetBoundsInScreen().CenterPoint();
  event_generator->MoveMouseTo(view_center);
  event_generator->ClickLeftButton();
}

void WaitForRecordingToStart() {
  auto* controller = CaptureModeController::Get();
  if (controller->is_recording_in_progress())
    return;
  base::RunLoop run_loop;
  ash::CaptureModeTestApi().SetOnVideoRecordingStartedCallback(
      run_loop.QuitClosure());
  run_loop.Run();
  ASSERT_TRUE(controller->is_recording_in_progress());
}

void StartVideoRecordingImmediately() {
  CaptureModeController::Get()->StartVideoRecordingImmediatelyForTesting();
  WaitForRecordingToStart();
}

base::FilePath WaitForCaptureFileToBeSaved() {
  base::FilePath result;
  base::RunLoop run_loop;
  ash::CaptureModeTestApi().SetOnCaptureFileSavedCallback(
      base::BindLambdaForTesting([&](const base::FilePath& path) {
        result = path;
        run_loop.Quit();
      }));
  run_loop.Run();
  return result;
}

base::FilePath CreateCustomFolderInUserDownloadsPath(
    const std::string& custom_folder_name) {
  base::FilePath custom_folder = CaptureModeController::Get()
                                     ->delegate_for_testing()
                                     ->GetUserDefaultDownloadsFolder()
                                     .Append(custom_folder_name);
  base::ScopedAllowBlockingForTesting allow_blocking;
  const bool result = base::CreateDirectory(custom_folder);
  DCHECK(result);
  return custom_folder;
}

base::FilePath CreateFolderOnDriveFS(const std::string& custom_folder_name) {
  auto* test_delegate = CaptureModeController::Get()->delegate_for_testing();
  base::FilePath mount_point_path;
  EXPECT_TRUE(test_delegate->GetDriveFsMountPointPath(&mount_point_path));
  base::FilePath folder_on_drive_fs =
      mount_point_path.Append("root").Append(custom_folder_name);
  base::ScopedAllowBlockingForTesting allow_blocking;
  const bool result = base::CreateDirectory(folder_on_drive_fs);
  EXPECT_TRUE(result);
  return folder_on_drive_fs;
}

void WaitForSeconds(int seconds) {
  base::RunLoop loop;
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE, loop.QuitClosure(), base::Seconds(seconds));
  loop.Run();
}

void SwitchToTabletMode() {
  TabletModeControllerTestApi test_api;
  test_api.DetachAllMice();
  test_api.EnterTabletMode();
}

void LeaveTabletMode() {
  TabletModeControllerTestApi().LeaveTabletMode();
}

void TouchOnView(const views::View* view,
                 ui::test::EventGenerator* event_generator) {
  DCHECK(view);
  DCHECK(event_generator);

  const gfx::Point view_center = view->GetBoundsInScreen().CenterPoint();
  event_generator->MoveTouch(view_center);
  event_generator->PressTouch();
  event_generator->ReleaseTouch();
}

void ClickOrTapView(const views::View* view,
                    const bool in_tablet_mode,
                    ui::test::EventGenerator* event_generator) {
  if (in_tablet_mode)
    TouchOnView(view, event_generator);
  else
    ClickOnView(view, event_generator);
}

views::Widget* GetCaptureModeBarWidget() {
  auto* session = CaptureModeController::Get()->capture_mode_session();
  DCHECK(session);
  return session->GetCaptureModeBarWidget();
}

CaptureModeBarView* GetCaptureModeBarView() {
  auto* session = CaptureModeController::Get()->capture_mode_session();
  DCHECK(session);
  return CaptureModeSessionTestApi(session).GetCaptureModeBarView();
}

UserNudgeController* GetUserNudgeController() {
  auto* session = CaptureModeController::Get()->capture_mode_session();
  DCHECK(session);
  return CaptureModeSessionTestApi(session).GetUserNudgeController();
}

bool IsLayerStackedRightBelow(ui::Layer* layer, ui::Layer* sibling) {
  DCHECK_EQ(layer->parent(), sibling->parent());
  const auto& children = layer->parent()->children();
  const int sibling_index =
      base::ranges::find(children, sibling) - children.begin();
  return sibling_index > 0 && children[sibling_index - 1] == layer;
}

void SetDeviceScaleFactor(float dsf) {
  auto* display_manager = Shell::Get()->display_manager();
  const auto display_id = display_manager->GetDisplayAt(0).id();
  display_manager->UpdateZoomFactor(display_id, dsf);
  auto* controller = CaptureModeController::Get();
  if (controller->is_recording_in_progress()) {
    CaptureModeTestApi().FlushRecordingServiceForTesting();
    auto* test_delegate = static_cast<TestCaptureModeDelegate*>(
        controller->delegate_for_testing());
    // Consume any pending video frame from before changing the DSF prior to
    // proceeding.
    test_delegate->RequestAndWaitForVideoFrame();
  }
}

views::Widget* EnableAndGetAutoClickBubbleWidget() {
  auto* autoclick_controller = Shell::Get()->autoclick_controller();
  autoclick_controller->SetEnabled(true, /*show_confirmation_dialog=*/false);
  Shell::Get()
      ->accessibility_controller()
      ->GetFeature(A11yFeatureType::kAutoclick)
      .SetEnabled(true);

  views::Widget* autoclick_bubble_widget =
      autoclick_controller->GetMenuBubbleControllerForTesting()
          ->GetBubbleWidgetForTesting();
  EXPECT_TRUE(autoclick_bubble_widget->IsVisible());
  return autoclick_bubble_widget;
}

void PressKeyOnVK(ui::test::EventGenerator* event_generator,
                  ui::KeyboardCode key_code,
                  int flags,
                  int source_device_id) {
  DispatchVKEvent(event_generator, /*is_press=*/true, key_code, flags,
                  source_device_id);
}

void ReleaseKeyOnVK(ui::test::EventGenerator* event_generator,
                    ui::KeyboardCode key_code,
                    int flags,
                    int source_device_id) {
  DispatchVKEvent(event_generator, /*is_press=*/false, key_code, flags,
                  source_device_id);
}

void PressAndReleaseKeyOnVK(ui::test::EventGenerator* event_generator,
                            ui::KeyboardCode key_code,
                            int flags,
                            int source_device_id) {
  PressKeyOnVK(event_generator, key_code, flags, source_device_id);
  ReleaseKeyOnVK(event_generator, key_code, flags, source_device_id);
}

gfx::Image ReadAndDecodeImageFile(const base::FilePath& image_path) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  // No need to read the image file, if the path doesn't exist.
  if (!base::PathExists(image_path)) {
    return gfx::Image();
  }

  std::string image_data;
  if (!base::ReadFileToString(image_path, &image_data)) {
    LOG(ERROR) << "Failed to read PNG file from disk.";
    return gfx::Image();
  }

  gfx::Image image = gfx::Image::CreateFrom1xPNGBytes(
      base::MakeRefCounted<base::RefCountedString>(std::move(image_data)));

  if (image.IsEmpty()) {
    LOG(ERROR) << "Failed to decode PNG file.";
  }

  return image;
}

TabSliderButton* GetImageToggleButton() {
  auto* controller = CaptureModeController::Get();
  DCHECK(controller->IsActive());
  auto* capture_type_view = GetCaptureModeBarView()->GetCaptureTypeView();
  return capture_type_view ? capture_type_view->image_toggle_button() : nullptr;
}

TabSliderButton* GetVideoToggleButton() {
  auto* controller = CaptureModeController::Get();
  DCHECK(controller->IsActive());
  auto* capture_type_view = GetCaptureModeBarView()->GetCaptureTypeView();
  return capture_type_view ? capture_type_view->video_toggle_button() : nullptr;
}

TabSliderButton* GetFullscreenToggleButton() {
  auto* controller = CaptureModeController::Get();
  DCHECK(controller->IsActive());
  auto* capture_source_view = GetCaptureModeBarView()->GetCaptureSourceView();
  return capture_source_view ? capture_source_view->fullscreen_toggle_button()
                             : nullptr;
}

TabSliderButton* GetRegionToggleButton() {
  auto* controller = CaptureModeController::Get();
  DCHECK(controller->IsActive());
  auto* capture_source_view = GetCaptureModeBarView()->GetCaptureSourceView();
  return capture_source_view ? capture_source_view->region_toggle_button()
                             : nullptr;
}

TabSliderButton* GetWindowToggleButton() {
  auto* controller = CaptureModeController::Get();
  DCHECK(controller->IsActive());
  auto* capture_source_view = GetCaptureModeBarView()->GetCaptureSourceView();
  return capture_source_view ? capture_source_view->window_toggle_button()
                             : nullptr;
}

PillButton* GetStartRecordingButton() {
  auto* controller = CaptureModeController::Get();
  DCHECK(controller->IsActive());
  return GetCaptureModeBarView()->GetStartRecordingButton();
}

IconButton* GetSettingsButton() {
  auto* controller = CaptureModeController::Get();
  DCHECK(controller->IsActive());
  return GetCaptureModeBarView()->settings_button();
}

IconButton* GetCloseButton() {
  auto* controller = CaptureModeController::Get();
  DCHECK(controller->IsActive());
  return GetCaptureModeBarView()->close_button();
}

const message_center::Notification* GetPreviewNotification() {
  const message_center::NotificationList::Notifications notifications =
      message_center::MessageCenter::Get()->GetVisibleNotifications();
  for (const message_center::Notification* notification : notifications) {
    if (notification->id() == kScreenCaptureNotificationId) {
      return notification;
    }
  }
  return nullptr;
}

void ClickOnNotification(std::optional<int> button_index) {
  const message_center::Notification* notification = GetPreviewNotification();
  CHECK(notification);
  notification->delegate()->Click(button_index, std::nullopt);
}

void AddFakeCamera(const std::string& device_id,
                   const std::string& display_name,
                   const std::string& model_id,
                   media::VideoFacingMode camera_facing_mode) {
  CameraDevicesChangeWaiter waiter;
  GetTestDelegate()->video_source_provider()->AddFakeCamera(
      device_id, display_name, model_id, camera_facing_mode);
  waiter.Wait();
}

void RemoveFakeCamera(const std::string& device_id) {
  CameraDevicesChangeWaiter waiter;
  GetTestDelegate()->video_source_provider()->RemoveFakeCamera(device_id);
  waiter.Wait();
}

void AddDefaultCamera() {
  AddFakeCamera(kDefaultCameraDeviceId, kDefaultCameraDisplayName,
                kDefaultCameraModelId);
}

void RemoveDefaultCamera() {
  RemoveFakeCamera(kDefaultCameraDeviceId);
}

size_t WaitForCameraAvailabilityWithTimeout(base::TimeDelta time_out) {
  CaptureModeTestApi test_api;
  int available_camera_num = test_api.GetNumberOfAvailableCameras();
  if (available_camera_num) {
    return available_camera_num;
  }
  base::RunLoop run_loop;
  const base::Time start_time = base::Time::Now();
  base::RepeatingTimer polling_timer;
  polling_timer.Start(
      FROM_HERE, base::Milliseconds(100), base::BindLambdaForTesting([&]() {
        available_camera_num = test_api.GetNumberOfAvailableCameras();
        base::TimeDelta time_difference = base::Time::Now() - start_time;
        if (available_camera_num > 0 || time_difference > time_out) {
          polling_timer.Stop();
          run_loop.Quit();
        }
      }));
  run_loop.Run();
  return available_camera_num;
}

void SelectCaptureModeRegion(ui::test::EventGenerator* event_generator,
                             const gfx::Rect& region_in_screen,
                             bool release_mouse) {
  auto* controller = CaptureModeController::Get();
  ASSERT_TRUE(controller->IsActive());
  ASSERT_EQ(CaptureModeSource::kRegion, controller->source());
  event_generator->set_current_screen_location(region_in_screen.origin());
  event_generator->PressLeftButton();
  event_generator->MoveMouseTo(region_in_screen.bottom_right());
  if (release_mouse) {
    event_generator->ReleaseLeftButton();
  }
  auto capture_region_in_root = region_in_screen;
  wm::ConvertRectFromScreen(controller->capture_mode_session()->current_root(),
                            &capture_region_in_root);
  EXPECT_EQ(capture_region_in_root, controller->user_capture_region());
}

// -----------------------------------------------------------------------------
// ProjectorCaptureModeIntegrationHelper:

ProjectorCaptureModeIntegrationHelper::ProjectorCaptureModeIntegrationHelper() =
    default;

void ProjectorCaptureModeIntegrationHelper::SetUp() {
  annotator_helper_.SetUp();
  auto* projector_controller = ProjectorController::Get();
  projector_controller->SetClient(&projector_client_);
  ON_CALL(projector_client_, StopSpeechRecognition)
      .WillByDefault(testing::Invoke([]() {
        ProjectorController::Get()->OnSpeechRecognitionStopped(
            /*forced=*/false);
      }));

  // Simulate the availability of speech recognition.
  SpeechRecognitionAvailability availability;
  availability.on_device_availability =
      OnDeviceRecognitionAvailability::kAvailable;
  ON_CALL(projector_client_, GetSpeechRecognitionAvailability)
      .WillByDefault(testing::Return(availability));
  EXPECT_CALL(projector_client_, IsDriveFsMounted())
      .WillRepeatedly(testing::Return(true));
}

bool ProjectorCaptureModeIntegrationHelper::CanStartProjectorSession() const {
  return ProjectorController::Get()->GetNewScreencastPrecondition().state !=
         NewScreencastPreconditionState::kDisabled;
}

void ProjectorCaptureModeIntegrationHelper::StartProjectorModeSession() {
  auto* projector_session = ProjectorSession::Get();
  EXPECT_FALSE(projector_session->is_active());
  auto* projector_controller = ProjectorController::Get();
  EXPECT_CALL(projector_client_, MinimizeProjectorApp());
  projector_controller->StartProjectorSession(
      base::SafeBaseName::Create("projector_data").value());
  EXPECT_TRUE(projector_session->is_active());
  auto* controller = CaptureModeController::Get();
  EXPECT_EQ(controller->source(), CaptureModeSource::kFullscreen);
}

// -----------------------------------------------------------------------------
// ViewVisibilityChangeWaiter:

ViewVisibilityChangeWaiter ::ViewVisibilityChangeWaiter(views::View* view)
    : view_(view) {
  view_->AddObserver(this);
}

ViewVisibilityChangeWaiter::~ViewVisibilityChangeWaiter() {
  view_->RemoveObserver(this);
}

void ViewVisibilityChangeWaiter::Wait() {
  wait_loop_.Run();
}

void ViewVisibilityChangeWaiter::OnViewVisibilityChanged(
    views::View* observed_view,
    views::View* starting_view) {
  wait_loop_.Quit();
}

// -----------------------------------------------------------------------------
// CaptureNotificationWaiter:

CaptureNotificationWaiter::CaptureNotificationWaiter() {
  message_center::MessageCenter::Get()->AddObserver(this);
}

CaptureNotificationWaiter::~CaptureNotificationWaiter() {
  message_center::MessageCenter::Get()->RemoveObserver(this);
}

void CaptureNotificationWaiter::Wait() {
  run_loop_.Run();
}

void CaptureNotificationWaiter::OnNotificationAdded(
    const std::string& notification_id) {
  if (notification_id == kScreenCaptureNotificationId) {
    run_loop_.Quit();
  }
}

// -----------------------------------------------------------------------------
// CameraDevicesChangeWaiter:

CameraDevicesChangeWaiter::CameraDevicesChangeWaiter() {
  CaptureModeController::Get()->camera_controller()->AddObserver(this);
}

CameraDevicesChangeWaiter::~CameraDevicesChangeWaiter() {
  CaptureModeController::Get()->camera_controller()->RemoveObserver(this);
}

void CameraDevicesChangeWaiter::Wait() {
  loop_.Run();
}

void CameraDevicesChangeWaiter::OnAvailableCamerasChanged(
    const CameraInfoList& cameras) {
  ++camera_change_event_count_;
  loop_.Quit();
}

void CameraDevicesChangeWaiter::OnSelectedCameraChanged(
    const CameraId& camera_id) {
  ++selected_camera_change_event_count_;
}

}  // namespace ash